GitHub überprüft Repositorys auf bekannte Geheimnisformate, um die betrügerische Verwendung von Anmeldeinformationen zu verhindern, die versehentlich veröffentlicht wurden. Secret scanning erfolgt standardmäßig in öffentlichen Repositorys und öffentlichen npm-Paketen. Repositoryadministratorinnen und Organisationsbesitzerinnen können secret scanning auch für private Repositorys aktivieren. Als Dienstanbieter kannst du eine Partnerschaft mit GitHub eingehen, um deine Geheimnisformate in secret scanning einzubeziehen.
Wird in einer öffentlichen Quelle eine Übereinstimmung mit deinem Geheimnisformat gefunden, werden Nutzdaten an einen HTTP-Endpunkt deiner Wahl gesendet.
Wird in einem für secret scanning konfigurierten privaten Repository eine Übereinstimmung mit deinem Geheimnisformat gefunden, werden Repositoryadministratoren und Committer benachrichtigt und können das secret scanning-Ergebnis auf GitHub anzeigen und verwalten. Weitere Informationen findest du unter Verwalten von Warnungen aus der Geheimnisüberprüfung.
In diesem Artikel wird beschrieben, wie du als Dienstanbieter mit GitHub zusammenarbeiten und dem secret scanning-Partnerprogramm beitreten kannst.
Der secret scanning-Prozess
Im folgenden Diagramm siehst du eine Zusammenfassung des secret scanning-Prozesses für öffentliche Repositorys, bei dem alle Übereinstimmungen an einen Überprüfungsendpunkt des Dienstanbieters gesendet werden. Ein ähnlicher Prozess sendet Dienstanbietertoken, die in öffentlichen Paketen in der npm-Registrierung verfügbar gemacht werden.
Teilnehmen am secret scanning-Programm auf GitHub
- Wende dich zunächst an GitHub.
- Identifiziere die relevanten Geheimnisse, die du überprüfen möchtest, und erstelle reguläre Ausdrücke, um diese zu erfassen. Ausführlichere Informationen und Empfehlungen finden Sie unter „Identifizieren Ihrer geheimen Schlüssel und Erstellen regulärer Ausdrücke“ weiter unten.
- Erstelle für öffentlich ermittelte Geheimnisübereinstimmungen einen Benachrichtigungsdienst für Geheimnisse, der Webhooks von GitHub mit den Nutzdaten der Nachricht für secret scanning akzeptiert.
- Implementiere im Benachrichtigungsdienst für Geheimnisse eine Signaturüberprüfung.
- Implementiere im Benachrichtigungsdienst die Sperrung von Geheimnissen sowie Benutzerbenachrichtigungen.
- Stelle für falsch positive Ergebnisse Feedback bereit (optional).
Kontaktieren von GitHub
Sende eine E-Mail an [email protected], um den Registrierungsvorgang zu beginnen.
Daraufhin erhältst du Informationen zum secret scanning-Programm, und du wirst zur Bestätigung der Nutzungsbedingungen von GitHub aufgefordert.
Identifizieren von Geheimnissen und Erstellen regulärer Ausdrücke
Für eine Geheimnisüberprüfung benötigt GitHub die folgenden Informationen für jedes Geheimnis, das im secret scanning-Programm enthalten sein soll:
-
Ein eindeutig lesbarer Name für den Geheimnistyp. Dieser wird später zur Angabe des
Type
-Werts in der Nachrichtennutzlast verwendet. -
Einen regulären Ausdruck für die Suche des Geheimnistyps. Wir empfehlen Ihnen, so genau wie möglich zu sein, da dies dazu beiträgt, die Zahl der falsch positiven Ergebnisse zu verringern. Einige bewährte Methoden für hochwertige, identifizierbare Geheimnisse sind:
- Ein eindeutig definiertes Präfix
- Zufällige Zeichenfolgen mit hoher Entropie
- Eine 32-Bit-Prüfsumme
-
Ein Testkonto für Ihren Dienst. Dies ermöglicht es uns, Beispiele für die Geheimnisse zu generieren und zu analysieren, um falsch positive Ergebnisse weiter zu reduzieren.
-
Die URL des Endpunkts, an den Nachrichten von GitHub gesendet werden. Die URL muss nicht für jeden Geheimnistyp eindeutig sein.
Senden Sie diese Informationen an [email protected].
Erstellen eines Benachrichtigungsdiensts für Geheimnisse
Erstelle einen öffentlichen, über das Internet zugänglichen HTTP-Endpunkt unter der bereitgestellten URL. Wird eine Übereinstimmung mit deinem regulären Ausdruck öffentlich ermittelt, sendet GitHub an diesen Endpunkt eine HTTP-Nachricht vom Typ POST
.
Beispiel für Anforderungstext
[
{
"token":"NMIfyYncKcRALEXAMPLE",
"type":"mycompany_api_token",
"url":"https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt",
"source":"content"
}
]
Beim Nachrichtentext handelt es sich um ein JSON-Array, das ein oder mehrere Objekte enthält. Jedes davon stellt eine einzelne Geheimnisübereinstimmung dar. Dein Endpunkt sollte Anforderungen mit vielen Übereinstimmungen ohne Timeout verarbeiten können. Die Schlüssel für jede Geheimnisübereinstimmung lauten:
- token: Der Wert der Geheimnisübereinstimmung
- type: Der bereitgestellte eindeutige Name zum Identifizieren des regulären Ausdrucks
- url: Die öffentliche URL, unter der die Übereinstimmung gefunden wurde (kann leer gelassen werden)
- source: Der Ort, an dem das Token auf GitHub gefunden wurde
Die Liste der gültigen Werte für source
lautet:
- Inhalt
- 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
- Unbekannt
Implementieren einer Signaturüberprüfung im Benachrichtigungsdienst für Geheimnisse
Die HTTP-Anforderung an deinen Dienst enthält auch Header, die dringend zum Überprüfen verwendet werden sollten, ob die empfangenen Nachrichten wirklich von GitHub stammen und nicht schädlich sind.
Die beiden HTTP-Header, nach denen du suchen solltest, lauten:
Github-Public-Key-Identifier
: Welcherkey_identifier
von der API verwendet werden sollGithub-Public-Key-Signature
: Signatur der Payload
Du kannst den öffentlichen GitHub-Schlüssel für die Geheimnisüberprüfung unter https://api.github.com/meta/public_keys/secret_scanning abrufen und die Nachricht mithilfe des Algorithmus ECDSA-NIST-P256V1-SHA256
überprüfen. Der Endpunkt stellt mehrere key_identifier
und öffentliche Schlüssel bereit. Du kannst anhand des Werts von Github-Public-Key-Identifier
bestimmen, welcher öffentliche Schlüssel verwendet werden soll.
Hinweis: Wenn du eine Anforderung an den oben genannten Endpunkt für den öffentlichen Schlüssel sendest, überschreitest du möglicherweise die Ratenbegrenzung. Um keine Rateneinschränkungen zu erreichen, kannst du wie in den untenstehenden Beispielen vorgeschlagen ein personal access token (classic) (keine Umfänge erforderlich) oder ein fine-grained personal access token (nur automatischer Lesezugriff für öffentliche Repositorys erforderlich) bzw. eine bedingte Anforderung verwenden. Weitere Informationen findest du unter Erste Schritte mit der REST-API.
Hinweis: Die Signatur wurde mithilfe des unformatierten Nachrichtentexts generiert. Daher musst du auch für die Signaturüberprüfung den unformatierten Nachrichtentext verwenden, anstatt das JSON-Array zu analysieren und in eine Zeichenfolge umzuwandeln. So verhinderst du, die Nachricht neu ordnen oder Abstände ändern zu müssen.
Gesendeter Beispiel-HTTP-POST zum Überprüfen des Endpunkts
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/"}]
Mit den folgenden Codeausschnitten wird gezeigt, wie du eine Signaturüberprüfung durchführen kannst.
In den Codeausschnitten wird angenommen, dass du eine Umgebungsvariable namens GITHUB_PRODUCTION_TOKEN
mit einem generierten personal access token festgelegt hast, um keine Rateneinschränkungen zu erreichen. Das personal access token benötigt keine Umfänge oder Berechtigungen.
Beispiel für eine Überprüfung in 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
}
Beispiel für eine Überprüfung in 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)
Beispiel für eine Überprüfung in 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");
}
};
Implementieren der Sperrung von Geheimnissen und von Benutzerbenachrichtigungen im Benachrichtigungsdienst
Für die öffentlich ermittelte secret scanning kannst du den Benachrichtigungsdienst für Geheimnisse dahingehend erweitern, dass verfügbar gemachte Geheimnisse gesperrt und betroffene Benutzer*innen benachrichtigt werden. Die konkrete Implementierung im Benachrichtigungsdienst bleibt dir überlassen. Doch es wird empfohlen, alle Geheimnisse als öffentlich und kompromittiert zu betrachten, zu denen du eine Nachricht von GitHub erhalten hast.
Bereitstellen von Feedback für falsch positive Ergebnisse
Wir sammeln Feedback zur Gültigkeit aller identifizierten Geheimnisse in Partnerantworten. Wenn du daran teilnehmen möchtest, sende eine E-Mail an [email protected].
Die Nachricht, die du von uns zu einem Geheimnis erhältst, besteht aus einem JSON-Array mit dem Token, dem Typbezeichner und der Commit-URL. Informiere uns in deinem Feedback darüber, ob es sich bei dem erkannten Token um echte oder falsche Anmeldeinformationen handelte. Nutze für das Feedback eines der folgenden Formate.
Du kannst das unformatierte Token senden:
[
{
"token_raw": "The raw token",
"token_type": "ACompany_API_token",
"label": "true_positive"
}
]
Du kannst das Token auch in Form eines kryptografischen Hashs des unformatierten Tokens mit SHA-256 bereitstellen:
[
{
"token_hash": "The SHA-256 hashed form of the raw token",
"token_type": "ACompany_API_token",
"label": "false_positive"
}
]
Einige wichtige Punkte:
- Sende uns das Token entweder unformatiert („token_raw“) oder als Hash („token_hash“), nicht beides.
- Verwende zum Erstellen der Hashform des unformatierten Tokens nur SHA-256, keinen anderen Hashalgorithmus.
- Die Bezeichnung gibt an, ob es sich bei dem Token um ein echtes („true_positive“) oder ein falsch positives („false_positive“) Ergebnis handelt. Nur diese beiden Literalzeichenfolgen in Kleinbuchstaben sind zulässig.
Hinweis: Das Anforderungstimeout für Partner, die Daten zu falsch positiven Ergebnissen bereitstellen, ist höher (und liegt bei 30 Sekunden). Solltest du ein Timeout höher als 30 Sekunden benötigen, sende eine E-Mail an [email protected].