O GitHub faz a varredura de repositórios de formatos secretos conhecidos para evitar uso fraudulento de credenciais confirmadas acidentalmente. Secret scanning ocorre por padrão em repositórios públicos e pacotes npm públicos. Os administradores do repositório e os proprietários da organização também podem habilitar secret scanning em repositórios privados. Como provedor de serviço, você pode fazer parcerias com GitHub para que seus formatos de segredo estejam incluídos em nosso secret scanning.
Quando uma correspondência do seu formato secreto é encontrada em uma fonte pública, uma carga é enviada para um ponto de extremidade HTTP de sua escolha.
Quando uma correspondência do formato secreto é encontrada em um repositório privado configurado para secret scanning, os administradores do repositório e o committer são alertados e podem visualizar e gerenciar o resultado secret scanning em GitHub. Para obter mais informações, confira "Gerenciar alertas da verificação de segredo".
Este artigo descreve como você pode fazer parceria com GitHub como um provedor de serviço e juntar-se ao programa de parceiro de secret scanning.
O processo de secret scanning
O diagrama a seguir resume o processo de secret scanning para repositórios públicos, com qualquer correspondência enviada para o ponto de extremidade de verificação de um provedor de serviços. Um processo semelhante envia tokens de provedores de serviços expostos em pacotes públicos no registro npm.
Juntando-se ao programa de secret scanning em GitHub
- Entre em contato com GitHub para dar início ao processo.
- Identifique os segredos relevantes cuja varredura você deseja realizar e crie expressões regulares para capturá-los. Para obter informações e recomendações mais detalhadas, confira "Identificar seus segredos e criar expressões regulares" abaixo.
- Para correspondências de segredos encontradas publicamente, crie um serviço de alerta de segredo que aceite webhooks de GitHub que contenham a carga da mensagem de secret scanning.
- Implemente a verificação de assinatura em seu serviço de alerta secreto.
- Implemente revogação do segredo e notificação do usuário no seu serviço de alerta secreto.
- Fornece feedback sobre falsos positivos (opcional).
Entre em contato com GitHub para dar início ao processo
Para iniciar o processo de registro, envie um email para secret-scanning@github.com.
Você receberá detalhes do programa de secret scanning e você precisará aceitar os termos de participação de GitHub antes de prosseguir.
Identifique seus segredos e crie expressões regulares
Para fazer a varredura dos seus segredos, GitHub precisa das informações a seguir para cada segredo que você deseja que seja incluído no programa secret scanning:
-
Um nome único e legível para o tipo do segredo. Usaremos isso para gerar o valor
Type
no conteúdo da mensagem posteriormente. -
Uma expressão regular que encontra o tipo do segredo. Recomendamos que haja a maior precisão possível, pois isso ajudará a reduzir o número de falsos positivos. Algumas melhores práticas para segredos identificáveis de alta qualidade são:
- Um prefixo definido exclusivamente
- Sequências aleatórias de alta entropia
- Uma soma de verificação de 32 bits
-
Uma conta de teste para o seu serviço. Isso nos permitirá gerar e analisar exemplos dos segredos, reduzindo ainda mais os falsos positivos.
-
A URL do ponto de extremidade que recebe mensagens de GitHub. A URL não precisa ser única para cada tipo de segredo.
Envie essas informações para secret-scanning@github.com.
Crie um serviço de alerta secreto
Crie um ponto de extremidade HTTP público e acessível à internet na URL que você nos forneceu. Quando uma correspondência da expressão regular for encontrada publicamente, o GitHub enviará uma mensagem HTTP POST
ao seu ponto de extremidade.
Exemplo do corpo de solicitação
[
{
"token":"NMIfyYncKcRALEXAMPLE",
"type":"mycompany_api_token",
"url":"https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt",
"source":"content"
}
]
O corpo da mensagem é uma matriz JSON que contém um ou mais objetos, com cada objeto representando apenas uma correspondência de segredos. Seu ponto de extremidade deve ser capaz de lidar com solicitações com um grande número de correspondências sem exceder o tempo. As chaves para cada correspondência de segredos são:
- token: o valor da correspondência de segredos.
- tipo: o nome exclusivo que você forneceu para identificar a expressão regular.
- url: a URL de commit pública em que a correspondência foi encontrada (pode estar vazio)
- source: onde o token foi encontrado em GitHub.
A lista de valores válidos para source
são:
- Sumário
- Confirmar
- Pull_request_title
- Pull_request_description
- Pull_request_comment
- Issue_title
- Issue_description
- Issue_comment
- Discussion_title
- Discussion_body
- Discussion_comment
- Commit_comment
- Gist_content
- Gist_comment
- Npm
- Desconhecido
Implemente a verificação de assinatura em seu serviço de alerta secreto
A solicitação HTTP para o seu serviço também conterá cabeçalhos cujo uso recomendamos fortemente para validar que as mensagens recebidas são genuinamente do GitHub e não são mal-intencionadas.
Os dois cabeçalhos HTTP a procurar são:
Github-Public-Key-Identifier
: qualkey_identifier
usar de nossa APIGithub-Public-Key-Signature
: a assinatura do conteúdo
Recupere a chave pública da verificação de segredos do GitHub de https://api.github.com/meta/public_keys/secret_scanning e valide a mensagem usando o algoritmo ECDSA-NIST-P256V1-SHA256
. O ponto de extremidade fornecerá várias chaves públicas e key_identifier
. Você pode determinar qual chave pública usar com base no valor de Github-Public-Key-Identifier
.
Observação: quando você enviar uma solicitação ao ponto de extremidade da chave pública acima, poderá atingir limites de taxa. Para evitar atingir limites de taxa, você pode usar um personal access token (classic) (nenhum escopo necessário) ou um fine-grained personal access token (somente o acesso de leitura a repositórios públicos automáticos é necessário) conforme sugerido nos exemplos abaixo ou usar uma solicitação condicional. Para obter mais informações, confira "Introdução à API REST".
Observação: a assinatura foi gerada usando o corpo da mensagem bruta. Portanto, é importante que você também use o texto da mensagem não processada para validação da assinatura, em vez de analisar e criar strings do JSON a fim de evitar reorganizar a mensagem ou mudar de espaçamento.
HTTP POST de exemplo enviado para verificar o ponto de extremidade
POST / HTTP/2
Host: HOST
Accept: */*
Content-Length: 104
Content-Type: application/json
Github-Public-Key-Identifier: bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c
Github-Public-Key-Signature: MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg==
[{"source":"commit","token":"some_token","type":"some_type","url":"https://example.com/base-repo-url/"}]
Os snippets de código a seguir demonstram como você pode realizar a validação da assinatura.
Os exemplos de código presumem que você definiu uma variável de ambiente chamada GITHUB_PRODUCTION_TOKEN
com um personal access token gerado para evitar atingir os limites de taxa. O personal access token não precisa de escopos/permissões.
Exemplo de validação no Go
package main
import (
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net/http"
"os"
)
func main() {
payload := `[{"token":"some_token","type":"some_type","url":"some_url","source":"some_source"}]`
kID := "f9525bf080f75b3506ca1ead061add62b8633a346606dc5fe544e29231c6ee0d"
kSig := "MEUCIFLZzeK++IhS+y276SRk2Pe5LfDrfvTXu6iwKKcFGCrvAiEAhHN2kDOhy2I6eGkOFmxNkOJ+L2y8oQ9A2T9GGJo6WJY="
// Fetch the list of GitHub Public Keys
req, err := http.NewRequest("GET", "https://api.github.com/meta/public_keys/secret_scanning", nil)
if err != nil {
fmt.Printf("Error preparing request: %s\n", err)
os.Exit(1)
}
if len(os.Getenv("GITHUB_PRODUCTION_TOKEN")) == 0 {
fmt.Println("Need to define environment variable GITHUB_PRODUCTION_TOKEN")
os.Exit(1)
}
req.Header.Add("Authorization", "Bearer "+os.Getenv("GITHUB_PRODUCTION_TOKEN"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("Error requesting GitHub signing keys: %s\n", err)
os.Exit(2)
}
decoder := json.NewDecoder(resp.Body)
var keys GitHubSigningKeys
if err := decoder.Decode(&keys); err != nil {
fmt.Printf("Error decoding GitHub signing key request: %s\n", err)
os.Exit(3)
}
// Find the Key used to sign our webhook
pubKey, err := func() (string, error) {
for _, v := range keys.PublicKeys {
if v.KeyIdentifier == kID {
return v.Key, nil
}
}
return "", errors.New("specified key was not found in GitHub key list")
}()
if err != nil {
fmt.Printf("Error finding GitHub signing key: %s\n", err)
os.Exit(4)
}
// Decode the Public Key
block, _ := pem.Decode([]byte(pubKey))
if block == nil {
fmt.Println("Error parsing PEM block with GitHub public key")
os.Exit(5)
}
// Create our ECDSA Public Key
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
fmt.Printf("Error parsing DER encoded public key: %s\n", err)
os.Exit(6)
}
// Because of documentation, we know it's a *ecdsa.PublicKey
ecdsaKey, ok := key.(*ecdsa.PublicKey)
if !ok {
fmt.Println("GitHub key was not ECDSA, what are they doing?!")
os.Exit(7)
}
// Parse the Webhook Signature
parsedSig := asn1Signature{}
asnSig, err := base64.StdEncoding.DecodeString(kSig)
if err != nil {
fmt.Printf("unable to base64 decode signature: %s\n", err)
os.Exit(8)
}
rest, err := asn1.Unmarshal(asnSig, &parsedSig)
if err != nil || len(rest) != 0 {
fmt.Printf("Error unmarshalling asn.1 signature: %s\n", err)
os.Exit(9)
}
// Verify the SHA256 encoded payload against the signature with GitHub's Key
digest := sha256.Sum256([]byte(payload))
keyOk := ecdsa.Verify(ecdsaKey, digest[:], parsedSig.R, parsedSig.S)
if keyOk {
fmt.Println("THE PAYLOAD IS GOOD!!")
} else {
fmt.Println("the payload is invalid :(")
os.Exit(10)
}
}
type GitHubSigningKeys struct {
PublicKeys []struct {
KeyIdentifier string `json:"key_identifier"`
Key string `json:"key"`
IsCurrent bool `json:"is_current"`
} `json:"public_keys"`
}
// asn1Signature is a struct for ASN.1 serializing/parsing signatures.
type asn1Signature struct {
R *big.Int
S *big.Int
}
Exemplo de validação no Ruby
require 'openssl'
require 'net/http'
require 'uri'
require 'json'
require 'base64'
payload = <<-EOL
[{"token":"some_token","type":"some_type","url":"some_url","source":"some_source"}]
EOL
payload = payload
signature = "MEUCIFLZzeK++IhS+y276SRk2Pe5LfDrfvTXu6iwKKcFGCrvAiEAhHN2kDOhy2I6eGkOFmxNkOJ+L2y8oQ9A2T9GGJo6WJY="
key_id = "f9525bf080f75b3506ca1ead061add62b8633a346606dc5fe544e29231c6ee0d"
url = URI.parse('https://api.github.com/meta/public_keys/secret_scanning')
raise "Need to define GITHUB_PRODUCTION_TOKEN environment variable" unless ENV['GITHUB_PRODUCTION_TOKEN']
request = Net::HTTP::Get.new(url.path)
request['Authorization'] = "Bearer #{ENV['GITHUB_PRODUCTION_TOKEN']}"
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == "https")
response = http.request(request)
parsed_response = JSON.parse(response.body)
current_key_object = parsed_response["public_keys"].find { |key| key["key_identifier"] == key_id }
current_key = current_key_object["key"]
openssl_key = OpenSSL::PKey::EC.new(current_key)
puts openssl_key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), payload.chomp)
Exemplo de validação no JavaScript
const crypto = require("crypto");
const axios = require("axios");
const GITHUB_KEYS_URI = "https://api.github.com/meta/public_keys/secret_scanning";
/**
* Verify a payload and signature against a public key
* @param {String} payload the value to verify
* @param {String} signature the expected value
* @param {String} keyID the id of the key used to generated the signature
* @return {void} throws if the signature is invalid
*/
const verify_signature = async (payload, signature, keyID) => {
if (typeof payload !== "string" || payload.length === 0) {
throw new Error("Invalid payload");
}
if (typeof signature !== "string" || signature.length === 0) {
throw new Error("Invalid signature");
}
if (typeof keyID !== "string" || keyID.length === 0) {
throw new Error("Invalid keyID");
}
const keys = (await axios.get(GITHUB_KEYS_URI)).data;
if (!(keys?.public_keys instanceof Array) || keys.length === 0) {
throw new Error("No public keys found");
}
const publicKey = keys.public_keys.find((k) => k.key_identifier === keyID) ?? null;
if (publicKey === null) {
throw new Error("No public key found matching key identifier");
}
const verify = crypto.createVerify("SHA256").update(payload);
if (!verify.verify(publicKey.key, Buffer.from(signature, "base64"), "base64")) {
throw new Error("Signature does not match payload");
}
};
Implemente revogação do segredo e notificação do usuário no seu serviço de alerta secreto
Para secret scanning encontrado publicamente, você pode melhorar o seu serviço de alerta de segredo para revogar os segredos expostos e notificar os usuários afetados. Você define como implementa isso no seu serviço de alerta de segredo, mas recomendamos considerar qualquer segredo que GitHub envie mensagens de que é público e que está comprometido.
Fornece feedback sobre falsos positivos
Coletamos feedback sobre a validade dos segredos individuais detectados nas respostas do parceiro. Caso deseje participar, envie-nos um email para secret-scanning@github.com.
Quando relatamos segredos para você, enviamos uma matriz JSON com cada elemento que contém o token, o identificador de tipo e a URL dp commit. Quando você nos envia feedback, você nos envia informações sobre se o token detectado era uma credencial real ou falsa. Aceitamos comentários nos seguintes formatos.
Você pode nos enviar o token não processado:
[
{
"token_raw": "The raw token",
"token_type": "ACompany_API_token",
"label": "true_positive"
}
]
Você também pode fornecer o token em forma de hash após executar uma única forma de hash criptográfico do token não processado usando SHA-256:
[
{
"token_hash": "The SHA-256 hashed form of the raw token",
"token_type": "ACompany_API_token",
"label": "false_positive"
}
]
Alguns pontos importantes:
- Você deve enviar-nos apenas a forma não processada do token ("token_raw"), ou a forma em hash ("token_hash"), mas não ambos.
- Para a forma de hash do token não processado, você só pode usar SHA-256 para armazenar o token, e não qualquer outro algoritmo de hashing.
- A etiqueta indica se o token é verdadeiro ("true_positive") ou um falso positivo ("false_positive"). São permitidas apenas essas duas strings literais minúsculas.
Observação: nosso tempo limite de solicitação está definido para ser maior (ou seja, 30 segundos) para os parceiros que fornecem dados sobre falsos positivos. Caso você precise de um tempo limite superior a 30 segundos, envie-nos um email para secret-scanning@github.com.