Assim que seu servidor estiver configurado para receber cargas, ele ouvirá qualquer carga enviada para o ponto de extremidade que você configurou. Por motivos de segurança, você provavelmente vai querer limitar os pedidos para aqueles provenientes do GitHub. Existem algumas maneiras de fazer isso. Você poderia, por exemplo, optar por permitir solicitações do endereço IP do GitHub. No entanto, um método muito mais fácil é configurar um token secreto e validar a informação.
Você pode usar a API REST para gerenciar o repositório, a organização e os webhooks de aplicativo. Você pode listar entregas de webhook para um webhook ou obter e reenviar uma entrega individual para um webhook, que pode ser integrado a um aplicativo ou serviço externo. Você também pode usar a API REST para alterar a configuração do webhook. Por exemplo, você pode modificar a URL da carga, tipo de conteúdo, verificação de SSL e segredo. Para obter mais informações, consulte:
Definir seu token secreto
Você precisará configurar seu token secreto em dois lugares: no GitHub e no seu servidor.
Para definir seu token no GitHub:
- Navegue até o repositório em que você está configurando seu webhook.
- Abaixo do nome do repositório, clique em Configurações. Caso não consiga ver a guia "Configurações", selecione o menu suspenso , clique em Configurações.
- Na barra lateral esquerda, clique em Webhooks.
- Ao lado do webhook, clique em Editar.
- No campo "Segredo", digite uma cadeia de caracteres aleatória com alta entropia. Você pode gerar uma cadeia de caracteres com
ruby -rsecurerandom -e 'puts SecureRandom.hex(20)'
no terminal, por exemplo. - Clique em Atualizar Webhook.
Em seguida, configure uma variável de ambiente em seu servidor que armazene este token. Normalmente, isso é tão simples quanto executar:
export SECRET_TOKEN=YOUR-TOKEN
Nunca embuta o token em código no aplicativo.
Validando conteúdos do GitHub
Quando seu token secreto está definido, GitHub Enterprise Cloud o utiliza para criar uma assinatura de hash com cada carga. Essa assinatura de hash é incluída nos cabeçalhos de cada solicitação como x-hub-signature-256
.
Observação: para compatibilidade com versões anteriores, também incluímos o cabeçalho x-hub-signature
gerado usando a função de hash SHA-1. Se possível, recomendamos que você use o cabeçalho x-hub-signature-256
para aprimorar a segurança. Os exemplos abaixo demonstram o uso do cabeçalho x-hub-signature-256
.
Você deve calcular um hash usando o SECRET_TOKEN
e garantir que o resultado corresponda ao hash do GitHub Enterprise Cloud. GitHub Enterprise Cloud usa um código hash hexadecimal HMAC para calcular o hash.
Observação: as cargas de webhook podem conter caracteres Unicode. Se o seu idioma e a implementação de servidor especificarem uma codificação de caracteres, certifique-se de que você manipula a carga como UTF-8.
Sua linguagem e implementações do servidor podem ser diferentes deste código de exemplo. No entanto, há uma série de aspectos muito importantes a destacar:
-
Independentemente da implementação usada, a assinatura de hash começa com
sha256=
, usando a chave do token secreto e o corpo da carga. -
Não
==
recomendamosusar um operador simples. Um método comosecure_compare
oucrypto.timingSafeEqual
executa uma comparação de cadeia de caracteres de "tempo constante", que ajuda a atenuar determinados ataques de tempo contra operadores de igualdade regulares, ou loops regulares em linguagens otimizadas para JIT.
Valores de teste
Independentemente da linguagem de programação usada para implementar a verificação HMAC no código, você pode usar os seguintes valores secret
e payload
para verificar se a implementação está correta.
- secret: "É um segredo para todos"
- payload: "Olá, Mundo!"
Se a implementação estiver correta e usar o algoritmo SHA-256, as assinaturas geradas deverão corresponder aos seguintes valores de assinatura:
- signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
- x-hub-signature: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
Se a implementação estiver correta e usar o algoritmo SHA-1, as assinaturas geradas deverão corresponder aos seguintes valores de assinatura:
- signature: 01dc10d0c83e72ed246219cdd91669667fe2ca59
- x-hub-signature: sha1=01dc10d0c83e72ed246219cdd91669667fe2ca59
Exemplo de Ruby
Por exemplo, você pode definir a seguinte função verify_signature
:
def verify_signature(payload_body)
signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE_256'])
end
Em seguida, você pode chamá-lo quando receber um conteúdo de webhook:
post '/payload' do
request.body.rewind
payload_body = request.body.read
verify_signature(payload_body)
push = JSON.parse(payload_body)
"I got some JSON: #{push.inspect}"
end
Exemplo de Python
Por exemplo, você pode definir a seguinte função verify_signature
e chamá-la quando receber um conteúdo de webhook:
import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
"""Verify that the payload was sent from GitHub by validating SHA256.
Raise and return 403 if not authorized.
Args:
payload_body: original request body to verify (request.body())
secret_token: GitHub app webhook token (WEBHOOK_SECRET)
signature_header: header received from GitHub (x-hub-signature-256)
"""
if not signature_header:
raise HTTPException(status_code=403, detail="x-hub-signature-256 header is missing!")
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
expected_signature = "sha256=" + hash_object.hexdigest()
if not hmac.compare_digest(expected_signature, signature_header):
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
Exemplo de JavaScript
Por exemplo, você pode definir a seguinte função verifySignature
e chamá-la em qualquer ambiente JavaScript quando receber um conteúdo de webhook:
let encoder = new TextEncoder();
async function verifySignature(secret, header, payload) {
let parts = header.split("=");
let sigHex = parts[1];
let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };
let keyBytes = encoder.encode(secret);
let extractable = false;
let key = await crypto.subtle.importKey(
"raw",
keyBytes,
algorithm,
extractable,
[ "sign", "verify" ],
);
let sigBytes = hexToBytes(sigHex);
let dataBytes = encoder.encode(payload);
let equal = await crypto.subtle.verify(
algorithm.name,
key,
sigBytes,
dataBytes,
);
return equal;
}
function hexToBytes(hex) {
let len = hex.length / 2;
let bytes = new Uint8Array(len);
let index = 0;
for (let i = 0; i < hex.length; i += 2) {
let c = hex.slice(i, i + 2);
let b = parseInt(c, 16);
bytes[index] = b;
index += 1;
}
return bytes;
}
Exemplo de Typescript
Por exemplo, você pode definir a seguinte função verify_signature
e chamá-la quando receber um conteúdo de webhook:
import * as crypto from "crypto"; const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET; const verify_signature = (req: Request) => { const signature = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(JSON.stringify(req.body)) .digest("hex"); let trusted = Buffer.from(`sha256=${signature}`, 'ascii'); let untrusted = Buffer.from(req.headers.get("x-hub-signature-256"), 'ascii'); return crypto.timingSafeEqual(trusted, untrusted); }; const handleWebhook = (req: Request, res: Response) => { if (!verify_signature(req)) { res.status(401).send("Unauthorized"); return; } // The rest of your logic here };
import * as crypto from "crypto";
const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET;
const verify_signature = (req: Request) => {
const signature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");
let trusted = Buffer.from(`sha256=${signature}`, 'ascii');
let untrusted = Buffer.from(req.headers.get("x-hub-signature-256"), 'ascii');
return crypto.timingSafeEqual(trusted, untrusted);
};
const handleWebhook = (req: Request, res: Response) => {
if (!verify_signature(req)) {
res.status(401).send("Unauthorized");
return;
}
// The rest of your logic here
};