Atualizações automáticas e por que você precisa de mais uma camada de assinatura

Na Parte 1, concluímos a configuração do certificado Developer ID e da notarização. Com isso, você está pronto para entregar o app aos usuários pela primeira vez. Mas um app não termina após um único lançamento — você precisa continuar enviando novas versões que corrigem bugs e adicionam funcionalidades.

Para um app da Mac App Store, a App Store trata as atualizações para você. Um app distribuído diretamente não tem esse benefício, então você precisa incluir um recurso de atualização automática no próprio app. No macOS, o padrão de fato para esse papel é o framework de código aberto Sparkle. Com o Sparkle configurado, o app verifica periodicamente um “feed de atualizações (appcast)” e, se existir uma nova versão, notifica o usuário, faz o download e a instala.

Isso levanta uma questão. Você já assina o app com o certificado Developer ID criado na Parte 1, então por que você precisa de mais uma chave?

A razão é que as duas assinaturas verificam coisas diferentes.

  • Certificado Developer ID — usado pelo Gatekeeper do macOS para decidir “é seguro instalar este app?”
  • Chave EdDSA do Sparkle — usada pelo Sparkle dentro do app para decidir “o arquivo de atualização que acabei de baixar foi realmente feito pelo desenvolvedor deste app?

As atualizações automáticas são uma operação sensível à segurança: o app baixa um arquivo da internet e se sobrescreve. Se alguém interceptar o servidor de atualizações ou o caminho de comunicação e inserir um arquivo falso, isso se torna um problema sério. Para evitar isso, o Sparkle só aceita atualizações assinadas com uma chave privada que apenas o desenvolvedor possui e se recusa a instalar qualquer coisa cuja assinatura não corresponda. É efetivamente uma camada de verificação separada do certificado.

Neste artigo, criaremos o par de chaves EdDSA (Ed25519) que será usado para essa verificação.

Como na Parte 1, todos os nomes e caminhos (FocusTimer, example.com, etc.) são valores de exemplo. Na prática, substitua-os pelas informações do seu próprio app.

Pré-requisito — o Sparkle já deve estar adicionado ao app

Antes de criar a chave, o framework Sparkle já deve estar adicionado como dependência do seu projeto de app. Se ainda não estiver, adicione-o no Xcode via Swift Package Manager (SPM).

  1. Abra seu projeto no Xcode → File → Add Package Dependencies…
  2. Insira o endereço do repositório na caixa de busca: https://github.com/sparkle-project/Sparkle
  3. Defina a regra de versão como 2.x (o major mais recente) e adicione

Depois disso, compile o projeto uma vez para que o SPM baixe o pacote Sparkle. As ferramentas de linha de comando que vêm empacotadas com ele são a chave para o próximo passo.

Passo 1 — Localizar as ferramentas de linha de comando do Sparkle

O pacote Sparkle inclui ferramentas de linha de comando usadas para geração de chaves e assinatura. Essas ferramentas ficam dentro da pasta onde o SPM baixou o pacote, mas esse local varia dependendo da versão do Xcode e das configurações do DerivedData. Por isso, é mais seguro encontrá-lo diretamente.

SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData \
  -path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1)
echo "$SPARKLE_BIN"

Se um único caminho for impresso, funcionou. Se nada aparecer, você pulou o passo “compilar o projeto uma vez” acima — execute um build no Xcode e tente novamente.

Dentro desta pasta estão as seguintes ferramentas.

  • generate_keys — gera, faz backup, restaura e verifica a chave de assinatura (usada neste artigo)
  • sign_update — assina arquivos de atualização (usado durante lançamentos reais)
  • generate_appcast — gera o feed de atualizações (appcast.xml) (aparece na Parte 3)

Passo 2 — Gerar a chave de assinatura

Agora crie o par de chaves.

"$SPARKLE_BIN/generate_keys"

Uma saída semelhante à seguinte aparece.

Generating a new signing key...
A key has been generated and saved in your keychain. Add the SUPublicEDKey
key to the Info.plist of each app for which you intend to use Sparkle...

    <key>SUPublicEDKey</key>
    <string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>

Este único comando cria duas chaves.

  • Chave pública (public key) — o valor SUPublicEDKey mostrado na saída acima. Não é um segredo e é a chave que você vai incorporar no app.
  • Chave privada (private key) — não aparece na tela. É armazenada automaticamente no Keychain do macOS como um item chamado “Private key for signing Sparkle updates”. É um segredo verdadeiro que nunca deve ser deixado em disco como um arquivo de texto simples.

Incorporando a chave pública no app

Coloque a string da chave pública da saída no Info.plist do app. Para o app de exemplo, adicione as seguintes chaves ao FocusTimer-Info.plist.

<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
  • SUPublicEDKey — a chave pública que você acabou de gerar. O app usa essa chave para verificar a assinatura das atualizações baixadas.
  • SUFeedURL — o endereço do feed de atualizações. Este domínio ainda não existe; o criamos na Parte 3. Por enquanto, é apenas um placeholder.

Como a chave pública está incorporada no app e apenas o desenvolvedor possui a chave privada, o app só aceitará atualizações assinadas com a chave privada. Esta é a estrutura central da verificação de atualizações do Sparkle.

Passo 3 — Fazer backup da chave privada (obrigatório!)

Se você pular este passo, poderá se arrepender profundamente mais tarde.

A chave privada está armazenada no Keychain, então está tudo bem no computador que você está usando agora. Mas se você perder o computador, o disco falhar ou reinstalar o macOS, essa chave desaparece junto.

O que acontece se a chave privada for perdida? As atualizações assinadas com uma nova chave serão rejeitadas pelos apps dos usuários existentes (apps com a chave pública antiga incorporada). Em outras palavras, você nunca mais poderá enviar atualizações automáticas para os usuários que já estão executando seu app. Sua única opção seria dizer a cada usuário individualmente: “por favor, baixe a nova versão você mesmo e reinstale-a.”

Portanto, faça o backup da chave logo após criá-la.

"$SPARKLE_BIN/generate_keys" -x ~/focustimer-sparkle-private.key
cat ~/focustimer-sparkle-private.key

A string base64 de uma única linha impressa pelo cat é a chave privada. Exemplo:

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=

Armazene essa string como uma nota segura (secure note) em um gerenciador de senhas como o 1Password. Dê à nota um nome fácil de encontrar mais tarde, como FocusTimer Sparkle EdDSA Private Key.

Logo após confirmar o salvamento, exclua o arquivo de texto simples deixado no disco.

rm ~/focustimer-sparkle-private.key

A regra é nunca deixar a chave privada no disco como um arquivo de texto simples. Mantenha o backup apenas dentro de um gerenciador de senhas criptografado.

Uma armadilha importante — o símbolo % não faz parte da chave

Quando você imprime a chave com cat, o terminal (especialmente o zsh) pode acrescentar um símbolo % ao final da linha.

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=%

Esse % é apenas o indicador do shell de que “a saída terminou sem uma nova linha” — não faz parte da chave. Se você copiar esse % para o seu backup, a chave ficará corrompida quando você restaurá-la mais tarde. Uma string base64 geralmente termina com =, então exclua o % após o = ao salvar.

Passo 4 — Restaurando em outro computador

Quando você precisar compilar o app em um novo computador, coloque a chave privada que você fez backup de volta no Keychain.

echo "sua_string_base64_de_backup" > ~/focustimer-sparkle-private.key
"$SPARKLE_BIN/generate_keys" -f ~/focustimer-sparkle-private.key
rm ~/focustimer-sparkle-private.key

A opção -f significa “importar a chave no arquivo para o Keychain”. Depois que a restauração for concluída, exclua o arquivo de texto simples aqui também, imediatamente.

Passo 5 — Verificação

Verifique se a chave foi instalada corretamente.

"$SPARKLE_BIN/generate_keys" -p

Uma única linha contendo a chave pública é impressa. Esse valor deve corresponder exatamente ao valor que você colocou em SUPublicEDKey no Info.plist no Passo 2. Se forem diferentes, a chave pública incorporada no app e a chave de assinatura real estão fora de sincronia, e a verificação de atualização falhará.

Resumo da Parte 2

Se você acompanhou até aqui, agora tem o seguinte.

  • ✅ Um par de chaves EdDSA do Sparkle gerado (chave pública + chave privada)
  • ✅ A chave pública incorporada no Info.plist do app (SUPublicEDKey)
  • ✅ A chave privada com backup seguro em um gerenciador de senhas
  • ✅ Familiaridade com como restaurá-la em outro computador

O app agora tem uma forma de verificar “se uma atualização baixada é genuína”. Mas uma coisa ainda está faltando. Na Parte 2, você escreveu https://updates.example.com/appcast.xml para SUFeedURL, mas ainda não há nada nesse endereço.

Na próxima parte, criaremos o repositório público onde o feed de atualizações (appcast.xml) e os arquivos .dmg ficarão, o conectaremos a um domínio que controlamos e finalizaremos as configurações de build — concluindo a configuração inicial.