São duas da manhã. Seu app compila. Você assina. Empacota num DMG. Executa notarytool submit. A Apple diz “In Progress”. Você espera 5 minutos. 10. 20. Uma hora. Duas horas. A submission continua “In Progress”. Você vai dormir. Na manhã seguinte: Invalid.
Sem mais explicação que “The signature of the binary is invalid”. Para ambas as arquiteturas. Obrigado, Apple. Muito útil.
A notarização é um desses processos que funciona perfeitamente… até não funcionar. E quando falha, te deixa com um .dmg que o Gatekeeper não vai permitir abrir e um erro que não diz nada. Depois de brigar com isso por alguns dias com o Tokamak (meu app de menu bar para monitorar a cota do Claude), decidi documentar tudo que aprendi e escrever um linter para nunca mais passar por isso.
O que é a notarização (em linguagem clara)
Imagine que a Mac App Store é um shopping center com segurança. Mas você não quer vender no shopping — quer distribuir seu app diretamente, com seu próprio DMG. Uma barraquinha na rua.
A Apple diz: “Ok, pode. Mas antes passe pelo segurança.”
Esse segurança é a notarização. É um serviço automatizado da Apple que escaneia seu app assinado, verifica que não contém malware conhecido, e se está tudo bem, te dá um ticket. Esse ticket você gruda no seu DMG (stapler staple) e a partir de então, quando um usuário baixar e tentar abrir, o Gatekeeper vê o ticket e diz “pode passar”.
Sem esse ticket, o usuário vê isso:
“Tokamak.app” não pode ser aberto porque a Apple não consegue verificar se não contém software malicioso.
E seu app fica na mão.
Por que a Apple faz isso
Há duas razões. Uma legítima. Outra… bem.
A legítima: proteger os usuários. Antes da notarização (introduzida no macOS 10.14.5, 2019), qualquer pessoa podia distribuir um .app assinado com Developer ID e o macOS abria sem questionar. O code signing verificava a identidade do desenvolvedor, mas não escaneava o conteúdo. Se seu app assinado incluísse um keylogger, assinado e tudo se executava.
A notarização adiciona uma camada: a Apple escaneia o binário em busca de malware conhecido e comportamentos suspeitos antes de chegar ao usuário. Não é uma revisão humana como a App Store — é um sistema automatizado. Mas é algo.
A outra razão: controle. A Apple quer que você passe pela App Store Connect para tudo. A distribuição direta com Developer ID sempre foi o cidadão de segunda classe. A notarização é mais um passo nesse caminho de “você pode distribuir fora da App Store, mas vamos tornar isso desconfortável”.
Dito isso, a notarização é obrigatória desde o macOS 10.15 para todo app distribuído fora da App Store. Não é opcional. Seu app vai notarizado ou não abre. Ponto final.
Os 7 erros que vão fazer você perder horas
Depois de coletar erros próprios e dos fóruns de desenvolvedores, estes são os que mais doem:
1. Falta o --timestamp
Esse é o clássico. Seu codesign funciona perfeito localmente, o Gatekeeper não reclama, e a notarização retorna “The signature of the binary is invalid.”
| |
O secure timestamp prova que a assinatura foi feita enquanto o certificado era válido. Sem ele, a Apple não confia. É como assinar um contrato sem data — tecnicamente é válido, mas ninguém vai aceitar.
2. Certificado incorreto
Há três certificados que soam parecidos e fazem coisas diferentes:
| Certificado | Para que serve |
|---|---|
| Apple Development | Builds de debug (desenvolvimento local) |
| Apple Distribution | App Store e TestFlight |
| Developer ID Application | Distribuição direta + notarização |
Se você assinar com “Apple Development” e tentar notarizar, a Apple te manda embora. Você precisa do Developer ID Application. Parece óbvio, mas quando você já está há três horas tentando, não é.
3. Hardened Runtime desativado
A notarização exige que seu app use o Hardened Runtime. É uma proteção que impede que seu app faça coisas como injetar código em outros processos ou desabilitar proteções de memória.
| |
Sem --options runtime, a assinatura é válida mas a Apple rejeita. É como ir a uma entrevista com o currículo perfeito mas sem calças.
4. xcodebuild -exportArchive trava
Se você usar xcodebuild -exportArchive com method: developer-id, se prepare: trava. Indefinidamente. Sem output. Sem erro. Só… silêncio.
O problema é que exportArchive precisa de credenciais do Apple ID para contactar os servidores de distribuição, e se não as tem em cache (ou se há contas antigas corrompidas no Keychain), fica esperando uma autenticação interativa que nunca chega.
A solução: pular o exportArchive e fazer a assinatura manualmente.
| |
Gambiarra, sim. Mas funciona. E não trava.
5. codesign pede acesso ao Keychain… em silêncio
A primeira vez que você assina com um certificado novo, o codesign precisa de acesso à chave privada armazenada no Keychain. O macOS mostra um popup pedindo permissão.
O problema: se você executar codesign de um IDE, um script ou uma ferramenta como Claude Code, o popup pode não aparecer. Ou aparecer atrás de todas as janelas. E o codesign trava esperando sua resposta a um popup que você não vê.
Solução: na primeira vez, execute codesign diretamente do terminal. Clique em “Sempre Permitir” no popup. A partir de então, os scripts funcionarão sem perguntar.
6. A notarização demora… e demora… e demora
A Apple diz que “most uploads are processed within minutes.” A realidade:
- Normal: 2-15 minutos
- Primeira vez com Developer ID novo: até 1 hora
- Quando a Apple tem problemas: horas. Ou dias. Sem aviso.
Em fevereiro de 2026, há relatos nos fóruns da Apple de submissions presas em “In Progress” por mais de 16 horas. O serviço estava tecnicamente “operacional”, mas muitas submissions simplesmente não eram processadas.
Não há nada que você possa fazer exceto esperar e tentar novamente. Bem-vindo à developer experience da Apple.
7. Extended attributes que quebram a assinatura
O macOS adiciona extended attributes aos arquivos baixados (o famoso com.apple.quarantine). Se seu pipeline de build copia arquivos que têm esses atributos, a assinatura se invalida.
| |
É um detalhe que ninguém te conta até que te morde.
O linter: 25 verificações para não ficar vulnerável
Depois de acumular todos esses erros, escrevi um script que verifica 25 condições de notarização — tanto estáticas (seu código e configuração) quanto dinâmicas (os artefatos de build).
Se executa assim:
| |
E produz algo como:
── CRITICAL ──
✓ PASS C1: export-direct passa --entitlements para codesign
✗ FAIL C2: Release CODE_SIGN_IDENTITY não é Developer ID
✓ PASS C3: ENABLE_HARDENED_RUNTIME = YES em Release
── HIGH ──
✗ FAIL H1: export-direct NÃO executa xattr -cr
── MEDIUM ──
⚠ WARN M1: DMG não é assinado
✓ PASS M2: LSUIElement declarado em project.yml
⚠ WARN M4: Só faz staple do DMG, não do .app
── CERTIFICADOS E KEYCHAIN ──
✓ PASS P1: Certificado Developer ID Application no Keychain
✓ PASS P2: Perfil notarytool 'tokamak-notary' configurado
── ARTEFATOS ──
✓ PASS P13: Binary universal (x86_64 + arm64)
✓ PASS C2-V: Export assinado com Developer ID Application
✓ PASS C3-V: Hardened runtime ativo na assinatura do export
── RESUMO ──
Total: 25 verificações
✓ 18 passaram
✗ 2 falharam
⚠ 3 avisos
⊘ 2 puladas (artefatos de build ausentes)
✗ NÃO NOTARIZAR — há 2 falhas para corrigir primeiro.
As verificações se dividem em quatro níveis:
CRITICAL — Se alguma falhar, a notarização vai falhar com certeza:
--entitlementsnocodesign(se não passar, perde sandbox e network)CODE_SIGN_IDENTITY= Developer ID no Release- Hardened Runtime ativado
HIGH — Provavelmente vai falhar ou causar problemas:
xattr -crantes de assinar- Sem dylibs de debug no Release
MEDIUM — Pode funcionar, mas você se arrisca:
- DMG assinado (não obrigatório, mas recomendado)
LSApplicationCategoryTypepara App Store- Consistência de versões entre
AppInfoeproject.yml
ARTEFATOS — Verifica os builds gerados:
- Assinatura válida com Developer ID
- Hardened Runtime ativo na assinatura
- Binary universal (x86_64 + arm64)
- Sem
.DS_Storenem symlinks no bundle - Estrutura correta (
Contents/{MacOS,Resources,Info.plist})
O script é estático para as verificações de configuração (não precisa de builds), e dinâmico para os artefatos (você precisa ter feito make archive && make export-direct). Assim você pode executá-lo a qualquer momento como preflight check.
O fluxo completo
Para que você tenha uma ideia do processo do começo ao fim:
flowchart TD
subgraph build[" 🔨 Build "]
direction TB
A["xcodegen generate"] --> B["xcodebuild archive<br/>(Release, assinado)"]
end
subgraph sign[" ✍️ Assinatura "]
direction TB
C["Copiar .app do archive"] --> D["xattr -cr<br/>(limpar attributes)"]
D --> E["codesign --force<br/>--options runtime<br/>--timestamp<br/>--sign Developer ID"]
end
subgraph package[" 📦 Empacotamento "]
direction TB
F["hdiutil create<br/>(DMG formato UDZO)"]
end
subgraph notarize[" 🍎 Notarização "]
direction TB
G["notarytool submit<br/>--wait"] --> H{Accepted?}
H -->|Sim| I["stapler staple<br/>(colar ticket)"]
H -->|Não| J["notarytool log<br/>(ver erros)"]
J --> K["Corrigir e<br/>assinar novamente"]
end
subgraph lint[" 🔍 Preflight "]
direction TB
L["lint-notarize.sh<br/>(25 verificações)"]
end
build --> sign
sign --> lint
lint -->|✓ Tudo OK| package
lint -->|✗ Falhas| K
package --> notarize
K --> sign
I --> M["✅ DMG pronto<br/>para distribuir"]
E em comandos:
| |
Lições aprendidas às 3h da manhã
--timestampsempre. Sempre. Não há desculpa.- A primeira assinatura, no terminal. Para que apareça o popup do Keychain.
- Não use
exportArchivepara Developer ID. Copiar +codesignmanual. - Tenha paciência com a Apple. A notarização pode demorar minutos ou horas. Não há SLA.
- Um linter vale mais que cem tentativas. Os 30 segundos que o script demora te poupam horas de submissions falhadas.
A notarização é como o exame de direção: desconfortável, burocrática, e provavelmente necessária. Mas uma vez que você domina o processo, se torna só mais um passo do pipeline. Um passo que não te acorda mais às 3h da manhã se perguntando por que a Apple diz “Invalid” sem se dignar a explicar o motivo.
O script completo está no repo do Tokamak.