Skip to main content

Partnerprogramm für die Geheimnisüberprüfung

Als Dienstanbieter kannst du eine Partnerschaft mit GitHub eingehen, um deine geheimen Tokenformate durch die Geheimnisüberprüfung zu sichern, die nach versehentlichen Commits deines geheimen Formats sucht und an den Überprüfungsendpunkt eines Dienstanbieters gesendet werden kann.

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.

Diagramm: Prozess der Geheimnisüberprüfung und Senden von Übereinstimmungen an den Überprüfungsendpunkt eines Dienstanbieters.

Teilnehmen am secret scanning-Programm auf GitHub

  1. Wende dich zunächst an GitHub.
  2. Identifiziere die relevanten Geheimnisse, die du überprüfen möchtest, und erstelle reguläre Ausdrücke, um diese zu erfassen.
  3. 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.
  4. Implementiere im Benachrichtigungsdienst für Geheimnisse eine Signaturüberprüfung.
  5. Implementiere im Benachrichtigungsdienst die Sperrung von Geheimnissen sowie Benutzerbenachrichtigungen.
  6. Stelle für falsch positive Ergebnisse Feedback bereit (optional).

Kontaktieren von GitHub

Sende eine E-Mail an secret-scanning@github.com, 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:

  • Einen eindeutigen lesbaren 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. Je genauer dieser Ausdruck ist, desto weniger falsch positive Ergebnisse werden später erzeugt.
  • Die URL des Endpunkts, an den Nachrichten von GitHub gesendet werden. Diese muss nicht für jeden Geheimnistyp eindeutig sein.

Sende diese Informationen an secret-scanning@github.com.

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: Welcher key_identifier von der API verwendet werden soll
  • Github-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 secret-scanning@github.com.

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 secret-scanning@github.com.