Autenticando e assinando commits no GitHub com YubiKey no macOS, sem GPG

Chave SSH em arquivo no disco é um segredo copiável. Qualquer processo rodando com seu usuário consegue ler ~/.ssh/id_ed25519, e a partir daí faz push no seu nome em qualquer repositório onde você tem acesso. Backup do disco, sincronização acidental para a nuvem, malware — em todos esses casos o segredo sai da máquina sem você perceber.
Uma YubiKey resolve isso de forma diferente de como se costuma imaginar. Ela não guarda o arquivo da chave em um cofre: a chave privada é gerada dentro do token e nunca sai de lá. O que fica no seu disco é apenas uma referência. Sem o dispositivo físico plugado, esse arquivo não serve para nada.
Este guia configura uma única chave que faz as duas coisas — autenticação (clone e push) e assinatura de commits — usando FIDO2 puro. Sem GPG, sem agente de smartcard, sem PIN digitado no teclado. Só o toque no sensor.
Substitua voce@exemplo.com pelo seu e-mail verificado no GitHub em todos os comandos.
Por que FIDO2 e não GPG
O caminho tradicional para commits assinados é GPG, e há muito material sobre usar a applet OpenPGP da YubiKey para isso. Funciona, mas traz uma pilha considerável: gpg-agent, encaminhamento de socket, pinentry, chaveiro de confiança, sub-chaves, expiração, e o eterno “por que o agente parou de responder”.
O Git 2.34 trouxe uma alternativa: gpg.format ssh. A partir dele, o Git assina commits com uma chave SSH em vez de uma chave GPG. Como a YubiKey já sabe ser uma chave SSH via FIDO2 (o tipo ed25519-sk), a mesma chave serve para autenticar e para assinar. Uma chave, um registro, zero daemons.
A troca é que você perde algumas coisas que só o GPG faz: cadeia de confiança entre pessoas, criptografia de arquivos, revogação distribuída. Para o caso de uso “quero que meus commits apareçam como verificados no GitHub e meu push exija presença física”, nada disso é necessário.
sequenceDiagram
actor U as Você
participant G as git / ssh
participant Y as YubiKey (FIDO2)
participant H as GitHub
U->>G: git push
G->>G: lê o key handle em ~/.ssh
Note over G: handle não é chave privada,<br/>é só um ponteiro para a credencial
G->>Y: pede assinatura do desafio
Y-->>U: LED pisca
U->>Y: toque físico
Y->>Y: assina com a chave privada interna
Note over Y: chave privada nunca sai do token
Y-->>G: assinatura
G->>H: autentica com a assinatura
H-->>G: ok
Pré-requisitos
- YubiKey 5 com firmware 5.2.3 ou superior — antes disso a applet FIDO2 não faz Ed25519
- macOS com Homebrew
- Git 2.34 ou superior, para assinatura via SSH
O firmware é gravado de fábrica e não é atualizável. Se o seu estiver abaixo de 5.2.3, dá para seguir o guia trocando ed25519-sk por ecdsa-sk em todos os comandos. A verificação vem no fim do passo 1, porque a ferramenta que reporta a versão é instalada lá.
1. Instalação
Aqui está a primeira pedra no caminho: o OpenSSH que a Apple distribui no macOS não inclui o middleware FIDO2. O ssh-keygen -t ed25519-sk simplesmente falha, com uma mensagem que não explica o motivo. É preciso usar o build do Homebrew.
brew install openssh libfido2 ykman gh
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
exec zsh
Em Macs Intel o prefixo do Homebrew é /usr/local/bin em vez de /opt/homebrew/bin.
O que cada pacote faz aqui: openssh é o build com suporte a FIDO2, libfido2 é a biblioteca que conversa com o token via USB, ykman é a CLI de gerenciamento da YubiKey (usada no passo 2 e no diagnóstico) e gh é a CLI do GitHub, opcional mas conveniente para registrar as chaves sem abrir o navegador.
Confirme que o binário certo está sendo resolvido:
which -a ssh ssh-keygen
ssh -V
O caminho do Homebrew precisa aparecer antes de /usr/bin, e a versão não deve mencionar “Apple”. Se mencionar, o PATH não foi aplicado nesta sessão do shell.
Com o ykman disponível, confira o firmware do dispositivo:
ykman info
Se a versão for menor que 5.2.3, use ecdsa-sk no lugar de ed25519-sk daqui para frente.
Para quem prefere interface gráfica, o Yubico Authenticator (versão 7 ou superior) faz as mesmas operações do ykman e é útil para visualizar o que está armazenado no dispositivo. Os comandos deste guia assumem a CLI.
2. Preparando um YubiKey novo
Se o dispositivo acabou de sair da caixa, vale gastar dez minutos endurecendo a configuração antes de gerar qualquer chave. As credenciais de fábrica são públicas e documentadas.
Se a sua YubiKey já está em uso e configurada, pule para o passo 3.
Troque as credenciais padrão
O YubiKey tem PINs independentes por applet. Os que importam aqui são dois.
PIN do FIDO2 — é o que sites como GitHub, Google e AWS pedem quando você usa passkeys. Ele é definido no momento em que você cadastra a primeira passkey, ou pelo app de gerenciamento. Não confunda com o PIN do PIV.
Applet PIV (smart card) — só relevante se você for usar certificados ou SSH via PIV. As três credenciais de fábrica precisam ser trocadas:
ykman piv access change-management-key --generate --protect
ykman piv access change-puk
ykman piv access change-pin
A Management Key de fábrica é uma constante conhecida; --generate --protect gera uma nova e a guarda protegida pelo PIN, então você não precisa memorizá-la. O PUK padrão é 12345678 e o PIN padrão é 123456.
Este guia usa FIDO2, não PIV — se você não pretende usar PIV, tecnicamente pode pular esses três comandos. Mas deixar as credenciais de fábrica em um applet que você não usa também não é ideal: alguém com acesso físico ao dispositivo poderia provisionar certificados ali.
Reduza a superfície de ataque
Desative o Yubico OTP. É o protocolo antigo, em que o toque no sensor digita uma string de 44 caracteres onde estiver o cursor. Se você não usa, ele só serve para produzir texto aleatório dentro do seu editor quando você toca na chave sem querer:
ykman config usb --disable OTP
ykman config nfc --disable OTP
Se você nunca vai aproximar a chave de um celular, desativar o NFC por completo elimina a possibilidade de uso por proximidade sem que você perceba.
3. Geração da chave
ssh-keygen -t ed25519-sk \
-O resident \
-O application=ssh:github \
-C "voce@exemplo.com" \
-f ~/.ssh/id_ed25519_sk_github \
-N ""
Toque na YubiKey quando o LED piscar.
O que cada opção faz:
| Opção | Efeito |
|---|---|
-O resident | A credencial fica armazenada dentro da YubiKey, permitindo recuperá-la em uma máquina nova com ssh-keygen -K. Consome um slot (a YubiKey 5 tem cerca de 25) |
-O application=ssh:github | Rótulo da credencial no token. Precisa começar com ssh:. É o que aparece em ykman fido credentials list |
-N "" | Sem passphrase no arquivo de handle |
Uma opção que não está aí, de propósito: -O verify-required. Ela exige o PIN a cada operação e, em várias combinações de firmware e libfido2, quebra a assinatura ed25519-sk com o erro Couldn't sign message: invalid format. Se você quer PIN como segundo fator mesmo assim, use ecdsa-sk, que não sofre desse problema.
Vale repetir, porque é o ponto que mais confunde: o arquivo ~/.ssh/id_ed25519_sk_github não é uma chave privada. É um key handle — um ponteiro para a credencial dentro do token. Vazá-lo não expõe nada.
Teste a assinatura antes de seguir adiante:
echo teste | ssh-keygen -Y sign -f ~/.ssh/id_ed25519_sk_github -n teste
Deve pedir toque e imprimir um bloco BEGIN SSH SIGNATURE. Se falhar aqui, resolva antes de continuar — a seção de diagnóstico cobre os casos comuns.
4. Registro no GitHub
Um detalhe que passa batido: a mesma chave pública precisa ser registrada duas vezes, com tipos diferentes. O GitHub trata autenticação e assinatura como capacidades separadas. Registrada só como Authentication, seus commits ficam “Unverified”; registrada só como Signing, o push é recusado.
pbcopy < ~/.ssh/id_ed25519_sk_github.pub
Em https://github.com/settings/keys:
- New SSH key → Key type: Authentication Key → cole → Add
- New SSH key → Key type: Signing Key → cole a mesma → Add
Ou pela CLI:
gh auth login
gh ssh-key add ~/.ssh/id_ed25519_sk_github.pub --type authentication --title "YubiKey"
gh ssh-key add ~/.ssh/id_ed25519_sk_github.pub --type signing --title "YubiKey signing"
gh ssh-key list
5. Configuração do SSH
Em ~/.ssh/config:
Host github.com
User git
IdentityFile ~/.ssh/id_ed25519_sk_github
IdentitiesOnly yes
O IdentitiesOnly yes não é enfeite. Sem ele, o ssh oferece ao servidor todas as chaves que encontrar no diretório e no agente, uma por uma. Se você tem várias, o GitHub corta a conexão por excesso de tentativas antes de chegar na certa, e o erro que aparece é um genérico Too many authentication failures.
Opcionalmente, force SSH mesmo em remotes que já estão em HTTPS:
git config --global url."git@github.com:".insteadOf "https://github.com/"
6. Assinatura de commits
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_sk_github.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
printf '%s %s\n' "voce@exemplo.com" "$(cat ~/.ssh/id_ed25519_sk_github.pub)" > ~/.ssh/allowed_signers
Note que user.signingkey aponta para a chave pública. Isso parece errado à primeira vista, mas é assim que o Git funciona no modo SSH: ele usa o .pub para localizar a credencial correspondente, e a assinatura em si acontece dentro do token.
O allowed_signers serve apenas para verificação local — é o que faz git log --show-signature dizer “Good signature” em vez de “No principal matched”. O GitHub não lê esse arquivo; ele usa o registro do passo 4.
O e-mail no allowed_signers precisa ser o mesmo do user.email do Git e estar verificado na sua conta GitHub. Divergência aí é a causa mais comum de badge “Unverified” com tudo o mais correto.
7. Validação
ssh -T git@github.com
Esperado, após o toque: Hi <usuario>! You've successfully authenticated...
cd /tmp && git init teste-yk && cd teste-yk
git commit --allow-empty -m "test: yubikey signing"
git log --show-signature -1
Esperado: Good "git" signature for voce@exemplo.com.
Limpe depois:
cd /tmp && rm -rf teste-yk
8. Backup e recuperação
Este é o ponto do guia que merece mais atenção, porque a propriedade que torna a YubiKey segura é a mesma que torna a perda irreversível.
A chave privada não é exportável. Não existe backup dela. Nenhum. YubiKey perdida, quebrada ou resetada significa acesso perdido, e nada — nem suporte da Yubico, nem root na sua máquina, nem o GitHub — recupera aquela chave.
Segunda YubiKey (faça agora, não depois)
A única estratégia de recuperação é ter uma segunda chave configurada em paralelo. E “depois eu configuro” é o que transforma um dispositivo perdido em uma conta inacessível.
Repita os passos 3 e 4 com a segunda YubiKey, usando outro nome de arquivo:
ssh-keygen -t ed25519-sk \
-O resident \
-O application=ssh:github \
-C "voce@exemplo.com (backup)" \
-f ~/.ssh/id_ed25519_sk_github_backup \
-N ""
Registre a pública dessa segunda chave no GitHub, também nos dois tipos. Guarde o dispositivo reserva em local físico separado do primeiro — na mesma gaveta os dois se perdem juntos.
Adicione como identidade alternativa no ~/.ssh/config:
Host github.com
User git
IdentityFile ~/.ssh/id_ed25519_sk_github
IdentityFile ~/.ssh/id_ed25519_sk_github_backup
IdentitiesOnly yes
O ssh tenta na ordem e usa a que corresponder ao token plugado, então não é preciso trocar configuração ao alternar entre as chaves.
Guarde também os recovery codes do 2FA da conta GitHub. Eles são a via de acesso se você ficar sem nenhum dos dois dispositivos.
Máquina nova
Como a credencial é residente, a YubiKey basta — não é preciso copiar nada da máquina antiga:
cd ~/.ssh && ssh-keygen -K
Isso baixa os handles com nomes no formato id_ed25519_sk_rk_ssh:github. Renomeie para id_ed25519_sk_github e refaça os passos 5 e 6.
9. Descobrindo qual credencial é qual
Cenário comum: a YubiKey já foi configurada antes, por você em outra máquina ou para outra conta, e agora você não sabe o que tem dentro dela. Ou você tem duas contas — pessoal e de trabalho — e precisa saber qual credencial pertence a qual.
Identifique antes de mexer em qualquer coisa. Apagar a credencial errada é irreversível.
1. Descarte GPG e PIV como possibilidade
gpg --card-status # "General key info: [none]" = nenhuma chave OpenPGP no cartão
ykman piv info # sem CHUID e management key ainda no default = nenhum certificado PIV
Se os dois vierem vazios, a autenticação é FIDO2 puro (-sk), não GPG smartcard nem PIV. Isso já elimina metade dos caminhos de investigação.
2. Liste as credenciais residentes na FIDO2
ykman fido credentials list
Pede o PIN — rode em um terminal interativo de verdade, não via automação ou CI. Cada linha traz Credential ID, RP ID, Username e Display name.
As credenciais SSH aparecem com RP ID no formato ssh:<nome> — exatamente o valor que foi passado em -O application= na criação, como ssh:github ou ssh:github-trabalho. Linhas com RP ID do tipo github.com, google.com ou login.microsoft.com são passkeys de login pelo navegador (WebAuthn), não chaves SSH. Ignore-as para este fim.
3. Extraia as chaves públicas das credenciais SSH
cd ~/.ssh && ssh-keygen -K
Gera um par id_ed25519_sk_rk_ssh:<nome> e .pub para cada credencial ssh:* encontrada.
4. Compare o fingerprint com o que está cadastrado em cada conta
ssh-keygen -lf ~/.ssh/id_ed25519_sk_rk_ssh:github.pub
Confronte com o fingerprint SHA256 listado em https://github.com/settings/keys, trocando de conta se você tiver mais de uma. Ou teste direto, apontando para o arquivo específico:
ssh -T git@github.com -i ~/.ssh/id_ed25519_sk_rk_ssh:github
O toque seguido de Hi <usuario>! You've successfully authenticated confirma qual credencial pertence a qual conta, sem ambiguidade.
5. Normalize o nome e siga o guia
Renomeie o arquivo recuperado para o padrão usado aqui (id_ed25519_sk_github) e siga os passos 5 e 6.
10. Diagnóstico de erros
As mensagens do OpenSSH nesse fluxo são particularmente ruins, e algumas dizem coisas que não têm relação com o problema real. Vale conhecer as principais.
no FIDO SecurityKeyProvider specified
Você está usando o ssh-keygen da Apple. Volte ao passo 1 e corrija o PATH.
Couldn't sign message: invalid format
O token respondeu, mas o OpenSSH não conseguiu interpretar a resposta. Quase sempre é -O verify-required combinado com ed25519-sk.
Isole o problema com uma chave descartável (sem -O resident, então não consome slot):
ssh-keygen -t ed25519-sk -f /tmp/tk -N ""
echo teste | ssh-keygen -Y sign -f /tmp/tk -n teste
rm -f /tmp/tk /tmp/tk.pub
- Funcionou → o problema está em alguma das opções que você usou, provavelmente
verify-required. Regenere sem ela. - Falhou → o problema é de binário, libfido2 ou firmware. Rode
brew reinstall libfido2 openssh, façaunset SSH_SK_PROVIDER, e se persistir troque paraecdsa-sk.
incorrect passphrase supplied to decrypt private key
Mensagem enganosa. O OpenSSH reutiliza esse texto para qualquer falha FIDO2, e ela não tem relação nenhuma com passphrase de arquivo. As causas reais são PIN errado, toque que expirou, ou key handle que não corresponde à credencial no token.
Antes de tentar de novo, verifique o contador:
ykman fido info
PIN retries remaining abaixo de 8 confirma que o PIN está sendo rejeitado.
Atenção: o PIN do FIDO2 tem 8 tentativas. Esgotadas, a applet bloqueia e o único caminho de volta é
ykman fido reset, que apaga permanentemente todas as passkeys de todas as contas armazenadas no dispositivo — não só a do GitHub. Diagnostique a causa antes de tentar repetidamente.
Permission denied (publickey)
A chave não está registrada como Authentication Key no GitHub, ou o IdentityFile aponta para o arquivo errado.
gh ssh-key list
ssh -vvv -T git@github.com 2>&1 | grep -E "Offering|Authentications that can continue"
O Offering mostra qual arquivo o ssh está de fato tentando usar.
Handle não corresponde à credencial
ssh-keygen -lf ~/.ssh/id_ed25519_sk_github.pub
ykman fido credentials list
O fingerprint precisa aparecer na listagem. Se não aparecer, o arquivo é resquício de uma chave antiga — talvez de um dispositivo que foi resetado. Recupere a credencial residente com ssh-keygen -K, ou apague a órfã e regenere:
ykman fido credentials delete <credential-id>
Use sempre credentials delete, que remove uma credencial específica. Nunca ykman fido reset, que apaga tudo.
Commits aparecem “Unverified” no GitHub
Três causas, em ordem de frequência:
- A chave não foi registrada com o tipo Signing Key (o passo que se esquece)
- O
user.emaildo Git é diferente do e-mail verificado na conta GitHub - Falta o
git config --global gpg.format ssh
11. Modelo de segurança
Vale ser explícito sobre o que essa configuração protege e o que não protege. Sem verify-required, o fator de autenticação é presença física apenas:
- ✅ Malware remoto não consegue usar a chave — precisa de um toque no dispositivo, e não há como simular isso por software
- ✅ A chave privada não é extraível, nem com root na máquina
- ✅ Vazamento de backup do disco não expõe nada, porque o arquivo é só um handle
- ❌ Quem tiver a YubiKey em mãos consegue usar a chave
Esse último item é a consequência direta de não exigir PIN. Na prática significa: não deixe a YubiKey plugada em máquina desacompanhada. Se o seu ambiente não permite confiar nisso — máquina compartilhada, escritório aberto, laptop que viaja — use -O verify-required com ecdsa-sk para adicionar o PIN como segundo fator, aceitando digitar o PIN a cada operação.
