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.”

1
2
3
4
5
# ERRADO — assinatura válida localmente, Apple rejeita
codesign --force --options runtime --sign "Developer ID Application: ..." MeuApp.app

# CERTO — com timestamp do servidor da Apple
codesign --force --options runtime --timestamp --sign "Developer ID Application: ..." MeuApp.app

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:

CertificadoPara que serve
Apple DevelopmentBuilds de debug (desenvolvimento local)
Apple DistributionApp Store e TestFlight
Developer ID ApplicationDistribuiçã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.

1
2
# A flag --options runtime ativa o Hardened Runtime
codesign --force --options runtime --timestamp --sign "Developer ID Application: ..." MeuApp.app

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 1. Copiar .app do archive
cp -R build/Tokamak.xcarchive/Products/Applications/Tokamak.app build/export/

# 2. Assinar com codesign
codesign --force --options runtime --timestamp \
  --sign "Developer ID Application: Seu Nome (TEAM_ID)" \
  build/export/Tokamak.app

# 3. Criar DMG
hdiutil create -volname "MeuApp" -srcfolder build/export/MeuApp.app -format UDZO MeuApp.dmg

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.

1
2
# Limpar antes de assinar
xattr -cr MeuApp.app

É 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:

1
bash scripts/lint-notarize.sh

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:

  • --entitlements no codesign (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 -cr antes de assinar
  • Sem dylibs de debug no Release

MEDIUM — Pode funcionar, mas você se arrisca:

  • DMG assinado (não obrigatório, mas recomendado)
  • LSApplicationCategoryType para App Store
  • Consistência de versões entre AppInfo e project.yml

ARTEFATOS — Verifica os builds gerados:

  • Assinatura válida com Developer ID
  • Hardened Runtime ativo na assinatura
  • Binary universal (x86_64 + arm64)
  • Sem .DS_Store nem 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["&nbsp;&nbsp;&nbsp;✍️ Assinatura&nbsp;&nbsp;&nbsp;"]
        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["&nbsp;&nbsp;&nbsp;📦 Empacotamento&nbsp;&nbsp;&nbsp;"]
        direction TB
        F["hdiutil create<br/>(DMG formato UDZO)"]
    end

    subgraph notarize["&nbsp;&nbsp;&nbsp;🍎 Notarização&nbsp;&nbsp;&nbsp;"]
        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["&nbsp;&nbsp;&nbsp;🔍 Preflight&nbsp;&nbsp;&nbsp;"]
        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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1. Build + archive
make archive

# 2. Exportar + assinar (sem exportArchive, que trava)
make export-direct

# 3. Lint (preflight)
bash scripts/lint-notarize.sh

# 4. DMG
make dmg

# 5. Notarizar
make notarize
# → xcrun notarytool submit ... --wait
# → xcrun stapler staple ...

# 6. Distribuir
# O DMG já tem o ticket colado. Pronto para upload.

Lições aprendidas às 3h da manhã

  1. --timestamp sempre. Sempre. Não há desculpa.
  2. A primeira assinatura, no terminal. Para que apareça o popup do Keychain.
  3. Não use exportArchive para Developer ID. Copiar + codesign manual.
  4. Tenha paciência com a Apple. A notarização pode demorar minutos ou horas. Não há SLA.
  5. 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.