GitHub analyse les dépôts pour rechercher les formats de secret connus afin d’empêcher l’utilisation frauduleuse d’informations d’identification commitées par erreur. L’Secret scanning se produit par défaut sur les référentiels publics et les packages npm publics. Les administrateurs de référentiel et les propriétaires de l’organisation peuvent également activer l’secret scanning sur des référentiels privés. En tant que fournisseur de services, vous pouvez vous associer à GitHub pour ajouter vos formats de secret à notre secret scanning.
Quand une correspondance de votre format de secret est trouvée dans une source publique, une charge utile est envoyée au point de terminaison HTTP de votre choix.
Quand une correspondance de votre format de secret est trouvée dans un dépôt privé configuré pour l’secret scanning, les administrateurs du dépôt et le commiteur sont avertis, et peuvent voir et gérer le résultat de l’secret scanning sur GitHub. Pour plus d’informations, consultez « Gestion des alertes à partir de l’analyse des secrets ».
Cet article explique comment vous associer à GitHub en tant que fournisseur de services et rejoindre le programme de partenariat d’secret scanning.
Processus d’secret scanning
Le diagramme suivant récapitule le processus d’secret scanning pour les dépôts publics, toutes les correspondances étant envoyées au point de terminaison de vérification d’un fournisseur de services. Un processus similaire envoie des jetons de fournisseurs de services exposés dans des packages publics sur le registre npm.
Adhésion au programme d’secret scanning sur GitHub
- Contactez GitHub pour lancer le processus.
- Identifiez les secrets pertinents que vous voulez analyser et créez des expressions régulières pour les capturer. Pour plus d’informations et de recommandations, consultez Identifier vos secrets et créer des expressions régulières ci-dessous.
- Pour les correspondances de secret trouvées dans les référentiels publics, créez un service d’alerte de secret qui accepte les webhooks de GitHub contenant la charge utile du message secret scanning.
- Implémentez la vérification de signature dans votre service d’alerte de secret.
- Implémentez la révocation de secret et la notification utilisateur dans votre service d’alerte de secret.
- Fournissez des commentaires pour les faux positifs (facultatif).
Contacter GitHub pour lancer le processus
Pour lancer le processus d’inscription, envoyez un e-mail à secret-scanning@github.com.
Vous recevez ensuite les détails du programme d’secret scanning et devez accepter les conditions de participation de GitHub avant de continuer.
Identifier vos secrets et créer des expressions régulières
Pour rechercher vos secrets, GitHub a besoin des informations suivantes pour chaque secret à ajouter au programme d’secret scanning :
-
Un nom unique et lisible pour le type de secret. Nous l’utilisons par la suite pour générer la valeur
Type
dans la charge utile du message. -
Expression régulière qui recherche le type de secret. Nous vous recommandons d’être aussi précis que possible, parce que ceci permet de réduire le nombre de faux positifs. Voici quelques meilleures pratiques pour des secrets identifiables et de haute qualité :
- Préfixe défini de manière unique
- Chaînes aléatoires d’entropie élevée
- Une somme de contrôle 32 bits
-
Un compte de test pour votre service. Cela nous permettra de générer et d’analyser des exemples de secrets, ce qui réduira davantage les faux positifs.
-
URL du point de terminaison qui reçoit des messages de GitHub. L’URL n’a pas besoin d’être unique pour chaque type de secret.
Envoyez ces informations à secret-scanning@github.com.
Créer un service d’alerte de secret
Créez un point de terminaison HTTP public accessible sur Internet au niveau de l’URL que vous nous avez fournie. Quand une correspondance de votre expression régulière est trouvée publiquement, GitHub envoie un message HTTP POST
à votre point de terminaison.
Exemple de corps de demande
[
{
"token":"NMIfyYncKcRALEXAMPLE",
"type":"mycompany_api_token",
"url":"https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt",
"source":"content"
}
]
Le corps du message est un tableau JSON qui contient un ou plusieurs objets, chaque objet représentant une seule correspondance de secret. Votre point de terminaison doit pouvoir gérer les requêtes ayant un grand nombre de correspondances sans expiration de délai. Les clés de chaque correspondance de secret sont les suivantes :
- jeton : valeur de la correspondance de secret.
- type : nom unique que vous avez fourni pour identifier votre expression régulière.
- url : URL publique où la correspondance a été trouvée (peut être vide)
- source : emplacement où le jeton a été trouvé sur GitHub.
La liste des valeurs valides pour source
est la suivante :
- Contenu
- Validation
- 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
- Inconnu
Implémenter la vérification de signature dans votre service d’alerte de secret
La requête HTTP envoyée à votre service contient également des en-têtes que nous vous recommandons vivement d’utiliser pour vérifier les messages que vous recevez proviennent bien de GitHub et qu’ils ne sont pas malveillants.
Les deux en-têtes HTTP à rechercher sont les suivants :
Github-Public-Key-Identifier
:key_identifier
à utiliser à partir de notre APIGithub-Public-Key-Signature
: signature de la charge utile
Vous pouvez récupérer la clé publique de l’analyse des secrets GitHub sur https://api.github.com/meta/public_keys/secret_scanning et valider le message avec l’algorithme ECDSA-NIST-P256V1-SHA256
. Le point de terminaison fournit plusieurs key_identifier
et clés publiques. Vous pouvez déterminer la clé publique à utiliser en fonction de la valeur de Github-Public-Key-Identifier
.
Note
Quand vous envoyez une demande au point de terminaison de clé publique ci-dessus, vous risquez d’atteindre les limites de débit. Pour éviter d’atteindre les limites de débit, vous pouvez utiliser un personal access token (classic) (aucune étendue nécessaire) ou un fine-grained personal access token (seuls les dépôts publics automatiques sont accessibles en lecture), comme indiqué dans les exemples ci-dessous, ou utiliser une requête conditionnelle. Pour plus d’informations, consultez « Prise en main de l’API REST ».
Note
La signature a été générée à partir du corps du message brut. Vous devez donc utiliser également le corps du message brut pour la validation de signature, au lieu d’analyser et de stringifier le JSON, pour éviter de réorganiser le message ou de changer l’espacement.
Exemple de requête HTTP POST envoyée pour vérifier le point de terminaison
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/"}]
Les extraits de code suivants montrent comment vous pouvez effectuer la validation de signature.
Les exemples de code supposent que vous avez défini une variable d’environnement appelée GITHUB_PRODUCTION_TOKEN
avec un personal access token généré pour éviter d’atteindre les limites de débit. Le personal access token n’a besoin d’aucune étendue/autorisation.
Exemple de validation dans 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
}
Exemple de validation dans 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)
Exemple de validation dans 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");
}
};
Implémenter la révocation de secret et la notification utilisateur dans votre service d’alerte de secret
Pour l’secret scanning dans les référentiels publics, vous pouvez renforcer votre service d’alerte de secret en lui permettant de révoquer les secrets exposés et d’avertir les utilisateurs affectés. Vous pouvez choisir la façon dont vous implémentez ces fonctionnalités dans votre service d’alerte de secret, mais nous vous recommandons de considérer tous les secrets pour lesquels GitHub vous envoie des messages les indiquant comme étant publics et compromis.
Fournir des commentaires pour les faux positifs
Nous collectons des commentaires sur la validité des secrets individuels détectés dans les réponses des partenaires. Si vous voulez participer, envoyez-nous un e-mail à secret-scanning@github.com.
Quand nous vous signalons des secrets, nous envoyons un tableau JSON dont chaque élément contient le jeton, l’identificateur du type et l’URL de commit. Quand vous nous envoyez des commentaires, vous nous indiquez si le jeton détecté correspondait à des informations d’identification réelles ou erronées. Nous acceptons les commentaires dans les formats suivants.
Vous pouvez nous envoyer le jeton brut :
[
{
"token_raw": "The raw token",
"token_type": "ACompany_API_token",
"label": "true_positive"
}
]
Vous pouvez également fournir le jeton haché après avoir appliqué un hachage de chiffrement unidirectionnel du jeton brut avec SHA-256 :
[
{
"token_hash": "The SHA-256 hashed form of the raw token",
"token_type": "ACompany_API_token",
"label": "false_positive"
}
]
Voici quelques points importants :
- Vous devez nous envoyer uniquement la forme brute du jeton (« token_raw ») ou la forme hachée (« token_hash »), mais pas les deux.
- Pour la forme hachée du jeton brut, vous pouvez uniquement utiliser SHA-256 pour hacher le jeton et aucun autre algorithme de hachage.
- L’étiquette indique si le jeton est un vrai positif (« true_positive ») ou un faux positif (« false_positive »). Seules ces deux chaînes littérales en minuscules sont autorisées.
Note
Notre délai d’expiration de demande est défini sur une valeur supérieure (c’est-à-dire, 30 secondes) pour les partenaires qui fournissent des données sur les faux positifs. Si vous avez besoin d’un délai d’expiration supérieur à 30 secondes, envoyez-nous un e-mail à secret-scanning@github.com.