GitHub escanea los repositorios en busca de formatos secretos para prevenir el uso fraudulento de las credenciales que se confirmaron por accidente. Secret scanning se produce de forma predeterminada en repositorios públicos y paquetes npm públicos. Los administradores de repositorios y los propietarios de la organización también pueden habilitar secret scanning en repositorios privados. Como proveedor de servicios, puedes asociarte con GitHub para que tus formatos de secreto se incluyan en nuestro secret scanning.
Cuando se encuentra una coincidencia de tu formato secreto en un repositorio público, se envía una carga útil a un punto de conexión HTTP de tu elección.
Cuando se encuentra una coincidencia de tu formato de secreto en un repositorio privado, la cual esté configurada para el secret scanning, entonces los administradores del repositorio y el confirmante recibirán una alerta y podrán ver y administrar el resultado del secret scanning en GitHub. Para más información, consulta Administración de alertas del examen de secretos.
Este artículo describe la forma en la que puedes asociarte con GitHub como proveedor de servicios y unirte al programa asociado del secret scanning.
El proceso del secret scanning
El siguiente diagrama resume el proceso del secret scanning para los repositorios públicos y muestra como cualquier coincidencia se envía a la terminal de verificación de un proveedor de servicios. Un proceso similar envía tokens de proveedores de servicios expuestos en paquetes públicos en el registro npm.
Unirse al programa del secret scanning en GitHub
- Contacta a GitHub para iniciar el proceso.
- Identifica los secretos relevantes que quieres escanear y crea expresiones regulares para capturarlos. Para obtener información y recomendaciones más detalladas, consulta Identificar los secretos y crear expresiones regulares a continuación.
- Para las coincidencias de secretos que se encuentran en los repositorios públicos, crea un servicio de alerta de secretos que acepte webhooks de GitHub, el cual contenga la carga útil del mensaje de secret scanning.
- Implementa la verificación de firmas en tu servicio de alerta secreto.
- Implementa la revocación de secretos y las notificaciones al usuario en tu servicio de alerta de secretos.
- Proporciona retroalimentación para los falsos positivos (opcional).
Contacta a GitHub para iniciar el proceso
Para iniciar el proceso de inscripción, envía un correo electrónico a secret-scanning@github.com.
Recibirás los detalles del programa del secret scanning y necesitarás aceptar las condiciones de participación de GitHub antes de proceder.
Identifica tus secretos y crea expresiones regulares
Para escanear tus secretos, GitHub necesita la siguiente información de cada secreto que quieras incluir en el programa del secret scanning:
-
Un nombre único y legible para las personas para el tipo de secreto. Lo usaremos para generar el valor
Type
en la carga del mensaje más adelante. -
Una expresión regular que encuentre el tipo de secreto. Le recomendamos que sea tan preciso como sea posible, ya que esto ayudará a reducir la cantidad de falsos positivos. Algunos procedimientos recomendados para secretos identificables de alta calidad son:
- Prefijo definido de forma única
- Cadenas aleatorias de alta entropía
- Suma de comprobación de 32 bits
-
Una cuenta de prueba para el servicio. Esto nos permitirá generar y analizar ejemplos de los secretos, lo que reducirá aún más los falsos positivos.
-
La URL de la terminal que recibe mensajes de GitHub. La URL no tiene que ser única para cada tipo de secreto.
Envía esta información a secret-scanning@github.com.
Crea un servicio de alerta de secretos
Crea una terminal HTTP pública y accesible desde la internet en la URL que nos proporcionaste. Cuando se encuentre una coincidencia de tu expresión regular en un repositorio público, GitHub enviará un mensaje HTTP de tipo POST
a tu punto de conexión.
Cuerpo de solicitud de ejemplo
[
{
"token":"NMIfyYncKcRALEXAMPLE",
"type":"mycompany_api_token",
"url":"https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt",
"source":"content"
}
]
El cuerpo del mensaje es una matriz JSON que contiene uno o varios objetos, cada uno de los cuales representa una coincidencia de secreto única. El punto de conexión debe poder controlar solicitudes con una gran cantidad de coincidencias sin que se agote el tiempo de espera. Las claves de cada coincidencia de secreto son las siguientes:
- token: valor de la coincidencia del secreto.
- type: nombre único que proporcionaste para identificar la expresión regular.
- url: dirección URL pública en la que se encontró la coincidencia (puede estar vacía).
- source: lugar en el que se encontró el token en GitHub.
Esta es una lista de valores válidos para source
:
- Contenido
- Commit
- 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
- Unknown
Implementa la verificación de firmas en tu servicio de alerta de secretos
La solicitud HTTP al servicio también contendrá encabezados que recomendamos usar para validar que los mensajes que se reciben proceden realmente de GitHub y no son malintencionados.
Los dos encabezados HTTP que se van a buscar son:
Github-Public-Key-Identifier
: indica quékey_identifier
se va a usar desde nuestra API.Github-Public-Key-Signature
: indica la firma de la carga.
Puedes recuperar la clave pública de análisis de secretos de GitHub de https://api.github.com/meta/public_keys/secret_scanning y validar el mensaje mediante el algoritmo ECDSA-NIST-P256V1-SHA256
. El punto de conexión proporcionará varios elementos key_identifier
y claves públicas. Puedes determinar qué clave pública se va a usar en función del valor de Github-Public-Key-Identifier
.
Note
Al enviar una solicitud al punto de conexión de clave pública anterior, puede que alcances los límites de velocidad. Para evitar alcanzar los límites de frecuencia, puede usar un personal access token (classic) (no se requieren ámbitos) o un fine-grained personal access token (solo se requiere el acceso de lectura a los repositorios públicos automáticos) como se sugiere en los ejemplos siguientes, o bien usar una solicitud condicional. Para más información, consulta Introducción a la API REST.
Note
La firma se generó con el cuerpo del mensaje sin procesar. Así que es importante que también utilices el cuerpo del mensaje sin procesar para la validación de la firma en vez de interpretar y convertir en secuencias el JSON, para evitar volver a arreglar dicho mensaje o cambiar los espacios.
Código HTTP POST de ejemplo que se envía para comprobar un punto de conexión
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/"}]
En los fragmentos de código siguientes se muestra cómo realizar la validación de la firma.
Los ejemplos de código dan por supuesto que has establecido una variable de entorno denominada GITHUB_PRODUCTION_TOKEN
con un personal access tokengenerado para evitar alcanzar los límites de frecuencia. El personal access token no necesita ámbitos ni permisos.
Ejemplo de validación en 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 := `[{"source":"commit","token":"some_token","type":"some_type","url":"https://example.com/base-repo-url/"}]`
kID := "bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c"
kSig := "MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg=="
// 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
}
Ejemplo de validación en Ruby
require 'openssl'
require 'net/http'
require 'uri'
require 'json'
require 'base64'
payload = <<-EOL
[{"source":"commit","token":"some_token","type":"some_type","url":"https://example.com/base-repo-url/"}]
EOL
payload = payload
signature = "MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg=="
key_id = "bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c"
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)
Ejemplo de validación en 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");
}
};
Implementa la revocación de secretos y la notificación a usuarios en tu servicio de alerta de secretos
Para secret scanning en repositorios públicos, puedes ampliar tu servicio de alerta de secretos para que revoque los secretos expuestos y notifique a los usuarios afectados. Depende de ti el cómo implementas esto en tu servicio de alerta de secretos, pero te recomendamos considerar cualquier secreto del cual GitHub te envíe mensajes de que es público y está puesto en riesgo.
Proporciona retroalimentación sobre los falsos positivos
Recolectamos la retroalimentación sobre la validez de los secretos individuales que se detectan en las respuestas de los socios. Si quieres participar, envíanos un correo electrónico a secret-scanning@github.com.
Cuando te reportamos los secretos, enviamos un arreglo de JSON con cada elemento que contiene el token, identificador de tipo y URL de confirmación. Cuando envías retroalimentación, nos envías información sobre si el token que se detectó fue una credencial real o falsa. Aceptamos la retroalimentación en los siguientes formatos.
Puedes enviarnos el token sin procesar:
[
{
"token_raw": "The raw token",
"token_type": "ACompany_API_token",
"label": "true_positive"
}
]
También puedes proporcionar el token en forma de hash después de realizar un hash criptográfico de una sola vía para el token sin procesar utilizando SHA-256:
[
{
"token_hash": "The SHA-256 hashed form of the raw token",
"token_type": "ACompany_API_token",
"label": "false_positive"
}
]
Algunos puntos importantes:
- Solo debes enviarnos ya sea la forma sin procesar del token ("token raw") o la forma en hash ("token_hash"), pero no ambas.
- En el caso de la forma en hash del token sin procesar, solo puedes utilizar SHA-256 para crear el hash del token y no algún otro algoritmo.
- La etiqueta indica si un token es un positivo verdadero ("true_positive") o falso ("false_positive"). Solo se permiten estas secuencias en minúsculas.
Note
Nuestro tiempo de espera de solicitud está configurado en un valor mayor (es decir, 30 segundos) para los asociados que proporcionen datos sobre falsos positivos. Si necesitas un tiempo de espera superior a 30 segundos, envíanos un correo electrónico a secret-scanning@github.com.