Skip to content

Como assinar bundles OTA em React Native (code signing) ​

Atualizado em outubro de 2026.

Resposta curta: assinar um bundle OTA significa que a CLI assina o hash de cada arquivo do update com uma chave privada, e o app, que tem a chave pública gravada na build nativa, recusa qualquer arquivo cuja assinatura não confere. No hot-updater, a assinatura é RSA com SHA-256 e acontece no deploy. No Pipa no Ar, ela é gerenciada: criamos a chave do app, a chave privada nunca sai do nosso lado e você liga em quatro passos.

Por que assinar se o update já vai por HTTPS ​

O HTTPS protege o caminho entre o servidor e o aparelho. Ele não diz nada sobre quem gerou o arquivo nem se o arquivo foi trocado antes de chegar ao servidor de downloads. Um update OTA roda código no aparelho de todos os seus usuários, então vale fechar as outras portas:

  • um arquivo alterado no armazenamento ou num cache no meio do caminho;
  • um bundle publicado por fora do fluxo de deploy;
  • um erro de configuração que aponte o app para um servidor errado.

Com a assinatura ligada, o app só aplica arquivos assinados pela chave do seu app. Qualquer outra coisa é descartada antes de ser carregada.

Como a assinatura funciona no hot-updater ​

  1. No deploy, a CLI calcula o SHA-256 de cada arquivo do bundle (o JavaScript e os assets) e do manifest que lista todos eles.
  2. Cada hash é assinado com a chave privada, no esquema RSA PKCS#1 v1.5 com SHA-256.
  3. As assinaturas vão no manifest. O hash do próprio manifest é publicado já assinado.
  4. No aparelho, o SDK confere cada arquivo baixado com a chave pública que está na build nativa: no Info.plist do iOS e nos metadados do app no Android.

A regra que o SDK segue é simples:

Build nativaUpdate sem assinaturaUpdate assinado
Sem chave públicaAceita (confere só o hash)Recusa
Com chave públicaRecusaConfere a assinatura

Por isso a ordem dos passos importa: depois que uma build com a chave pública chega às lojas, todo update para ela precisa sair assinado.

O problema real: onde fica a chave privada ​

Assinar é a parte fácil. Difícil é guardar a chave.

Hospedando o hot-updater por conta própria, você gera o par com npx hot-updater keys generate e guarda a chave privada no computador de quem publica ou nos segredos do CI. Isso traz dois riscos: a chave vazar, e qualquer um com ela assinar updates; ou a chave se perder, e ninguém conseguir mais publicar para as builds que já estão nas lojas até sair uma build nova.

O hot-updater tem um caminho melhor, a assinatura remota: a CLI manda só o hash de cada arquivo (32 bytes) para um serviço que guarda a chave e devolve a assinatura. A chave privada nunca chega na máquina de quem publica.

Assinatura gerenciada no Pipa no Ar ​

O Pipa no Ar usa essa assinatura remota. Ao ativar, criamos um par de chaves RSA de 3072 bits só para o app. A chave privada fica cifrada do nosso lado e não pode ser baixada. No deploy, a CLI envia só os hashes, autenticada pelo token de deploy, e recebe as assinaturas.

Como ligar ​

  1. Ative no painel. No app, abra Assinatura e clique em Ativar assinatura.
  2. Ligue no config. No hot-updater.config.ts, passe signing: true para o pipanoar():
    ts
    import { bare } from "@hot-updater/bare";
    import { pipanoar } from "@pipanoar/hot-updater";
    import { defineConfig } from "hot-updater";
    
    export default defineConfig({
      build: bare({ enableHermes: true }),
      ...pipanoar({ appId: "app_xxxxxxxxxxxxxxxxxxxx", signing: true }),
      updateStrategy: "appVersion",
    });
  3. Coloque a chave pública na build nativa.
    • React Native bare: a CLI busca a chave e grava no Info.plist e no Android:
      sh
      npx hot-updater keys export-public
    • Expo: baixe a chave pública na página Assinatura e aponte a opção publicKeyPath do plugin @hot-updater/expo para o arquivo.
  4. Gere e publique uma build nativa nova. Daqui em diante, todo deploy sai assinado.

Com o signing: true no config, o comando de deploy não muda:

sh
npx hot-updater deploy -p ios -r 10

O passo a passo oficial, com os avisos, está em Assinatura de bundles.

A ordem importa ​

Builds antigas, sem a chave pública, continuam aceitando updates sem assinatura. Builds com a chave só aceitam updates assinados. Então ligue o signing: true antes de publicar o primeiro update para a build nova. Se o app recusar o update depois de ligar a assinatura, quase sempre o deploy saiu sem ela: confira o config e publique de novo. Ver Dúvidas e problemas comuns.

O que a assinatura não resolve ​

Seja direto com o seu time sobre o que muda:

  • O token de deploy continua sendo segredo. Na assinatura gerenciada, é ele que autoriza pedir assinaturas. Use um token com escopo Deploy, limitado a um app e a um pipeline, e revogue na hora se vazar. Ver Tokens e API keys.
  • Código nativo não muda pelo ar, assinado ou não. Mudanças nativas continuam indo pela loja.
  • Um bug assinado continua sendo um bug. Para isso existem o rollout progressivo e o rollback de um clique.

Trocar a chave ​

Troque a chave só se ela vazou. As builds que já estão nas lojas confiam só na chave antiga, então o roteiro é: trocar a chave na página Assinatura, gerar uma build nativa com a chave pública nova, publicar nas lojas e só então voltar a fazer deploy para essa build. O histórico de chaves fica na mesma página.

E se eu quiser sair do Pipa no Ar? ​

A assinatura usa o protocolo aberto do hot-updater. Para hospedar por conta própria, você gera a sua chave, coloca a pública numa build nova e publica com a sua infraestrutura. O roteiro completo está em Sem amarra.

Perguntas frequentes ​

Preciso de uma chave por plataforma? Não. A chave é do app, e o mesmo par vale para iOS e Android.

Posso ligar a assinatura num app que já está em produção? Pode, a partir de uma build nativa nova. Com o signing: true, os updates saem assinados, e uma build sem a chave pública recusa update assinado. Com o updateStrategy: "appVersion", a build nova costuma ter uma versão nativa nova, e os updates assinados vão só para ela.

A assinatura está em todos os planos? Sim, inclusive no Free. Veja os planos.