From f324130a70807676b9e5e9394bd99370e9d2b7c3 Mon Sep 17 00:00:00 2001 From: Carloswar Date: Fri, 10 Jul 2026 19:52:02 -0500 Subject: [PATCH] feat: firma de codigo para distribucion interna Firma Authenticode de la app y el instalador con certificado auto-firmado, para equipos controlados (no elimina SmartScreen en distribucion publica, que exige un certificado EV comprado; el pipeline sirve igual con uno). - tools/New-CodeSigningCert.ps1: crea el cert y exporta signing/*.{cer,pfx} - tools/sign.ps1: firma Margoth.exe y Margoth_Setup.exe con sello RFC3161, usando el cert del almacen (sin signtool) - tools/Trust-MargothCert.ps1: confia el cert publico en cada equipo destino - .gitignore: excluye signing/ y todo material de clave (*.pfx/*.cer/...) - README: flujo de firma (build->firmar->empaquetar->firmar->confiar) y nota para el usuario final sobre el aviso de editor Verificado: ambos binarios quedan firmados por 'CN=Carlos G' con sello de tiempo de DigiCert; el estado pasa a Valido en equipos que confian el cert. --- .gitignore | 7 ++++ README.md | 46 +++++++++++++++++++++++-- tools/New-CodeSigningCert.ps1 | 63 ++++++++++++++++++++++++++++++++++ tools/Trust-MargothCert.ps1 | 45 ++++++++++++++++++++++++ tools/sign.ps1 | 64 +++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 tools/New-CodeSigningCert.ps1 create mode 100644 tools/Trust-MargothCert.ps1 create mode 100644 tools/sign.ps1 diff --git a/.gitignore b/.gitignore index becf3c8..e2e5fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ build/ .env .venv venv/ + +# Material de firma de código: NUNCA subir claves privadas ni certificados +signing/ +*.pfx +*.cer +*.p12 +*.key diff --git a/README.md b/README.md index 7465e62..19a3e51 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,11 @@ necesitas instalar Python ni tener conexión a internet.** 1. Consigue el archivo **`Margoth_Setup.exe`** (te lo entrega quien distribuye la aplicación). 2. Haz doble clic en `Margoth_Setup.exe`. -3. Si Windows muestra el aviso azul *"Windows protegió tu PC"* (aparece porque - el instalador aún no está firmado digitalmente), haz clic en **"Más - información"** y luego en **"Ejecutar de todas formas"**. +3. Si Windows muestra el aviso azul *"Windows protegió tu PC"*, haz clic en + **"Más información"** y luego en **"Ejecutar de todas formas"**. + > Este aviso no aparece si antes se instaló en el equipo el certificado de + > confianza de Margoth (ver *Firma de código* en la sección de Desarrollo). + > En una implementación gestionada, quien instala la app ya lo dejó listo. 4. Sigue el asistente y pulsa *Instalar*. **No pide permisos de administrador**: se instala en tu carpeta de usuario. 5. Al terminar, abre **Margoth** desde el acceso directo del **Escritorio** o @@ -192,6 +194,44 @@ El ejecutable se generará en `dist/Margoth/`. 2. Abrir `margoth_installer.iss` en Inno Setup. 3. Compilar para obtener `dist/Margoth_Setup.exe`. +### Firma de código (equipos internos) + +La app y el instalador se firman con un certificado **auto-firmado**. Esto +sirve para distribución interna (equipos que tú controlas): tras confiar el +certificado en cada equipo, la firma es válida y desaparece el "Editor +desconocido". + +> ⚠️ Un certificado auto-firmado **no** elimina SmartScreen para usuarios +> externos/públicos. Para eso se necesita un certificado **EV** comprado +> (~US$300-600/año, con token). El pipeline de abajo funciona igual con un +> cert comprado: solo cambia el certificado usado. + +**Preparación (una sola vez, en el equipo de build):** +```powershell +# Crea el certificado y exporta signing\Margoth-CodeSigning.{cer,pfx} +tools\New-CodeSigningCert.ps1 -PfxPassword "" +``` +El `.pfx` (clave privada) es tu respaldo: guárdalo a salvo, **nunca lo subas** +(la carpeta `signing/` está en `.gitignore`). + +**En cada release:** +```powershell +python build_exe.py # 1. genera dist\Margoth\ +tools\sign.ps1 -AppOnly # 2. firma Margoth.exe +ISCC margoth_installer.iss # 3. empaqueta el .exe ya firmado +tools\sign.ps1 -InstallerOnly # 4. firma Margoth_Setup.exe +``` +Ambas firmas incluyen sello de tiempo RFC3161 (siguen válidas tras expirar el +certificado). + +**En cada equipo donde se instale Margoth (una vez):** +```powershell +# Reparte signing\Margoth-CodeSigning.cer (es público) y ejecútalo allí: +tools\Trust-MargothCert.ps1 +``` +Windows pedirá confirmar la instalación del certificado raíz (es normal). +Después, `Margoth_Setup.exe` mostrará al editor **Carlos G** como válido. + ## Principios de Diseño - **Cero sobrecarga cognitiva**: Interfaces minimalistas para pacientes (Teoría de Mayer) diff --git a/tools/New-CodeSigningCert.ps1 b/tools/New-CodeSigningCert.ps1 new file mode 100644 index 0000000..5b7fe2c --- /dev/null +++ b/tools/New-CodeSigningCert.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Crea un certificado de firma de código AUTO-FIRMADO para Margoth. + +.DESCRIPTION + Uso interno: firmar Margoth para equipos que tú controlas. NO sirve para + quitar SmartScreen en distribución pública (eso requiere un certificado EV + comprado). El certificado queda en el almacén del usuario (Cert:\CurrentUser\My) + y se exportan dos archivos en la carpeta `signing/`: + + - Margoth-CodeSigning.cer -> PÚBLICO. Se instala en los equipos destino + para "confiar" en la firma. + - Margoth-CodeSigning.pfx -> PRIVADO (con contraseña). Respáldalo; permite + firmar desde otro equipo. NO se sube al repo. + + Correr una sola vez. Válido por 5 años. + +.PARAMETER PfxPassword + Contraseña para proteger el .pfx exportado (respaldo de la clave privada). + +.PARAMETER Publisher + Nombre del editor (CN del certificado). Debe coincidir con AppPublisher del + instalador para que la identidad sea consistente. +#> +param( + [Parameter(Mandatory = $true)] + [string]$PfxPassword, + + [string]$Publisher = "Carlos G", + + [int]$YearsValid = 5 +) + +$ErrorActionPreference = "Stop" + +$root = Split-Path -Parent $PSScriptRoot +$outDir = Join-Path $root "signing" +New-Item -ItemType Directory -Force -Path $outDir | Out-Null + +Write-Host "Creando certificado de firma de código para '$Publisher'..." +$cert = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject "CN=$Publisher" ` + -FriendlyName "Margoth Code Signing" ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyUsage DigitalSignature ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears($YearsValid) + +Write-Host " Thumbprint: $($cert.Thumbprint)" + +$cerPath = Join-Path $outDir "Margoth-CodeSigning.cer" +$pfxPath = Join-Path $outDir "Margoth-CodeSigning.pfx" + +Export-Certificate -Cert $cert -FilePath $cerPath | Out-Null +$secure = ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText +Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $secure | Out-Null + +Write-Host "" +Write-Host "Listo:" +Write-Host " Público (repartir a equipos): $cerPath" +Write-Host " Privado (respaldar, NO subir): $pfxPath" +Write-Host " Thumbprint para firmar: $($cert.Thumbprint)" diff --git a/tools/Trust-MargothCert.ps1 b/tools/Trust-MargothCert.ps1 new file mode 100644 index 0000000..748f8ef --- /dev/null +++ b/tools/Trust-MargothCert.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS + Instala el certificado público de Margoth como CONFIABLE en este equipo. + +.DESCRIPTION + Correr UNA vez en cada equipo donde se vaya a instalar Margoth. Importa + `Margoth-CodeSigning.cer` a: + - Entidades de certificación raíz de confianza (valida la cadena) + - Editores de confianza (reconoce al editor) + + Tras esto, la firma de Margoth.exe y de Margoth_Setup.exe aparece como + VÁLIDA y desaparece el aviso de "Editor desconocido". + + NOTA: al importar a la raíz de confianza, Windows mostrará un cuadro de + seguridad pidiendo confirmación. Es normal: acepta para completar. + +.PARAMETER CerPath + Ruta al archivo .cer público. Por defecto, junto a este script en signing\. + +.PARAMETER AllUsers + Instala para TODOS los usuarios del equipo (requiere ejecutar como + administrador). Sin este switch, se instala solo para el usuario actual. +#> +param( + [string]$CerPath, + [switch]$AllUsers +) + +$ErrorActionPreference = "Stop" + +if (-not $CerPath) { + $root = Split-Path -Parent $PSScriptRoot + $CerPath = Join-Path $root "signing\Margoth-CodeSigning.cer" +} +if (-not (Test-Path $CerPath)) { + throw "No se encontró el certificado: $CerPath" +} + +$scope = if ($AllUsers) { "LocalMachine" } else { "CurrentUser" } +Write-Host "Instalando confianza del certificado ($scope) desde: $CerPath" + +Import-Certificate -FilePath $CerPath -CertStoreLocation "Cert:\$scope\Root" | Out-Null +Import-Certificate -FilePath $CerPath -CertStoreLocation "Cert:\$scope\TrustedPublisher" | Out-Null + +Write-Host "Listo. El certificado de Margoth ahora es de confianza en este equipo." diff --git a/tools/sign.ps1 b/tools/sign.ps1 new file mode 100644 index 0000000..dc608e2 --- /dev/null +++ b/tools/sign.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS + Firma (Authenticode) el ejecutable de Margoth y/o el instalador. + +.DESCRIPTION + Usa el certificado de firma de código del almacén del usuario + (Cert:\CurrentUser\My) sin necesidad de signtool ni de la contraseña del + .pfx. Agrega sello de tiempo RFC3161 para que la firma siga siendo válida + después de que el certificado expire. + + Por defecto firma ambos artefactos si existen: + - dist\Margoth\Margoth.exe (la app) + - dist\Margoth_Setup.exe (el instalador) + + Flujo recomendado de release: + 1. python build_exe.py # genera dist\Margoth\ + 2. tools\sign.ps1 -AppOnly # firma Margoth.exe + 3. ISCC margoth_installer.iss # empaqueta el .exe YA firmado + 4. tools\sign.ps1 -InstallerOnly # firma Margoth_Setup.exe + +.PARAMETER Subject + CN del certificado a usar (por defecto "Carlos G"). + +.PARAMETER TimestampServer + Servidor de sello de tiempo RFC3161. +#> +param( + [string]$Subject = "Carlos G", + [string]$TimestampServer = "http://timestamp.digicert.com", + [switch]$AppOnly, + [switch]$InstallerOnly +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot + +$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | + Where-Object { $_.Subject -eq "CN=$Subject" } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + +if (-not $cert) { + throw "No se encontró un certificado de firma con CN=$Subject. Corre primero tools\New-CodeSigningCert.ps1." +} +Write-Host "Firmando con: $($cert.Subject) [$($cert.Thumbprint)]" + +$targets = @() +if (-not $InstallerOnly) { $targets += (Join-Path $root "dist\Margoth\Margoth.exe") } +if (-not $AppOnly) { $targets += (Join-Path $root "dist\Margoth_Setup.exe") } + +foreach ($file in $targets) { + if (-not (Test-Path $file)) { + Write-Host " (omitido, no existe) $file" + continue + } + $res = Set-AuthenticodeSignature -FilePath $file -Certificate $cert ` + -TimestampServer $TimestampServer -HashAlgorithm SHA256 + Write-Host " $($res.Status) -> $file" + if ($res.Status -ne "Valid") { + throw "La firma de $file falló: $($res.StatusMessage)" + } +} + +Write-Host "Firma completada."