Skip to main content

Партнерская программа сканирования секретов

Поставщики услуг могут сотрудничать с GitHub для защиты форматов секретных маркеров с помощью проверки секретов, в рамках которой выполняется поиск случайных фиксаций в формате секрета, результаты которого можно отправить в конечную точку проверки поставщика услуг.

GitHub сканирует репозитории известных форматов секретов, чтобы предотвратить случайное использование учетных данных, которые были зафиксированы случайно. Secret scanning по умолчанию выполняется в общедоступных репозиториях и общедоступных пакетах npm. Администраторы репозитория и владелец организации также могут включать secret scanning в частных репозиториях. Как поставщик услуг вы можете сотрудничать с GitHub, чтобы форматы секретов были включены в наше secret scanning.

Если совпадение формата секрета найдено в общедоступном источнике, полезные данные отправляются в конечную точку HTTP по вашему выбору.

При обнаружении соответствия формата секрета в частном репозитории, настроенном для secret scanning, об этом оповещаются администраторы репозитория и средство фиксации, которые могут просматривать результат secret scanning в GitHub. Дополнительные сведения см. в разделе Управление оповещениями о проверке секретов.

В этой статье описывается способ сотрудничества с GitHub в качестве поставщика услуг и присоединения к партнерской программе secret scanning.

Процесс secret scanning

На следующей схеме показан процесс secret scanning для общедоступных репозиториев с любыми совпадениями, отправленными в конечную точку проверки поставщика услуг. Аналогичный процесс отправляет маркеры поставщиков услуг, предоставляемые в общедоступных пакетах в реестре npm.

Схема, показывающая процесс сканирования секрета и отправки совпадений в конечную точку поставщика услуг.

Присоединение программы secret scanning на GitHub

  1. Чтобы начать процесс, обратитесь к GitHub.
  2. Определите соответствующие секреты, которые необходимо сканировать, и создайте регулярные выражения для их записи.
  3. Для открытых совпадений секретов создайте службу предупреждений секрета, которая принимает веб-перехватчики из GitHub с полезными данными secret scanning.
  4. Реализуйте проверку подписи в службе оповещений о секретах.
  5. Реализуйте отзыв секретов и уведомление пользователя в службе оповещений о секретах.
  6. Предоставьте отзыв о ложноположительных результатах (необязательно).

Чтобы начать процесс, обратитесь к GitHub

Чтобы начать процесс регистрации, отправьте сообщение электронной почты secret-scanning@github.com.

Вы получите сведения о программе secret scanning, и перед продолжением необходимо согласиться с условиями участия GitHub.

Определение секретов и создание регулярных выражений

Для сканирования секретов GitHub требуются следующие фрагменты информации для каждого секрета, который требуется включить в программу secret scanning:

  • Понятное уникальное имя для типа секрета. Мы будем использовать его позже для создания значения Type в полезных данных сообщения.
  • Регулярное выражение, которое позволяет найти тип секрета. Будьте как можно точнее, так как это сократит число ложноположительных результатов.
  • URL-адрес конечной точки, получающей сообщения от GitHub. Он не обязательно должен быть уникальным для каждого типа секрета.

Отправьте эти сведения в secret-scanning@github.com.

Создание службы оповещений о секрете

Создайте общедоступную в Интернете конечную точку HTTP по URL-адресу, который вы нам предоставили. Когда совпадение регулярного выражения найдено публично, GitHub отправляет HTTP-сообщение POST в конечную точку.

Примеры текста запроса

[
  {
    "token":"NMIfyYncKcRALEXAMPLE",
    "type":"mycompany_api_token",
    "url":"https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt",
    "source":"content"
  }
]

Текст сообщения — это массив JSON, содержащий один или несколько объектов, с каждым объектом, представляющим одно совпадение секретов. Конечная точка должна иметь возможность обрабатывать запросы с большим количеством совпадений без истечения времени ожидания. Ключи для каждого соответствия секрета:

  • токен: значение совпадения секрета.
  • тип: уникальное имя, предоставленное для идентификации регулярного выражения.
  • URL-адрес: общедоступный URL-адрес, в котором найден совпадение (может быть пустым)
  • источник: где маркер найден на GitHub.

Список допустимых значений source :

  • content
  • фиксация
  • 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
  • неизвестно

Реализуйте проверку подписи в службе оповещений о секретах

HTTP-запрос к вашей службе также будет содержать заголовки, которые мы настоятельно рекомендуем использовать для проверки получаемых сообщений из GitHub, и не являются вредоносными.

Два заголовка HTTP для поиска:

  • Github-Public-Key-Identifier: что key_identifier следует использовать из нашего API
  • Github-Public-Key-Signature: подпись полезных данных

Открытый ключ сканирования секрета GitHub можно получить из https://api.github.com/meta/public_keys/secret_scanning и проверить сообщение с помощью алгоритма ECDSA-NIST-P256V1-SHA256. Конечная точка предоставляет несколько key_identifier и открытых ключей. Вы можете определить, какой открытый ключ следует использовать на основе значения Github-Public-Key-Identifier.

Примечание. При отправке запроса в конечную точку вышеупомянутого открытого ключа можно достигнуть ограничений скорости. Чтобы избежать ограничений скорости попадания, можно использовать personal access token (classic) (нет необходимых область) или fine-grained personal access token (только автоматический доступ на чтение общедоступных репозиториев) как показано в приведенных ниже примерах или использовать условный запрос. Дополнительные сведения см. в разделе Начало работы с REST API.

Примечание: подпись была создана с помощью необработанного текста сообщения. Поэтому так важно для проверки подписи использовать необработанный текст сообщения, а не синтаксический анализ и преобразование JSON в строку, чтобы избежать изменения содержимого сообщения или изменения интервала.

Пример HTTP POST, отправляемый для проверки конечной точки

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/"}]

В следующих фрагментах кода показано, как можно выполнить проверку подписи. В примерах кода предполагается, что вы задали переменную среды, вызываемую GITHUB_PRODUCTION_TOKEN с созданными personal access token , чтобы избежать ограничений скорости попадания. Для personal access token не требуется никаких область/разрешений.

Пример проверки в 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
}

Пример проверки в 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)

Пример проверки в 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");
  }
};

Реализуйте отзыв секретов и уведомление пользователя в службе оповещений о секретах

Для secret scanning вы можете улучшить службу оповещений секретов, чтобы отозвать предоставленные секреты и уведомить затронутых пользователей. Как вы реализуете это в своей службе оповещения о секретах зависит от вас. Рекомендуется учитывать все секреты, о которых GitHub отправляет вам сообщения как общедоступные и скомпрометированные.

Отправка отзыва о ложноположительных результатах

Мы собираем отзывы о допустимости обнаруженных отдельных секретах в ответах партнера. Если вы хотите принять участие в этом, отправьте нам письмо по адресу secret-scanning@github.com.

При передаче вам секретов мы отправляем массив JSON с каждым элементом, содержащим маркер, идентификатор типа и URL-адрес фиксации. При передаче нам вашего отзыва вы отправляете нам сведения о том, является ли обнаруженный маркер реальными или ложными учетными данными. Мы принимаем отзывы в следующих форматах.

Вы можете отправить нам необработанный маркер:

[
  {
    "token_raw": "The raw token",
    "token_type": "ACompany_API_token",
    "label": "true_positive"
  }
]

Вы также можете указать маркер в хэшированных формах после выполнения одностороннего криптографического хэша необработанного маркера с помощью SHA-256:

[
  {
    "token_hash": "The SHA-256 hashed form of the raw token",
    "token_type": "ACompany_API_token",
    "label": "false_positive"
  }
]

Некоторые важные моменты:

  • Вы должны отправить нам либо необработанную форму маркера ("token_raw"), либо хэшированную форму ("token_hash"), но не обе одновременно.
  • Для хэшированной формы необработанного маркера вы можете использовать только SHA-256 для хэширования маркера, а не любой другой хэш-алгоритм.
  • Метка указывает на то, является ли маркер истинноположительным ("true_positive") или ложноположительным результатом ("false_positive"). Допускаются только эти две строки литерала в нижнем регистре.

Примечание: для партнеров, которые предоставляют данные о ложноположительных результатах, наше время ожидания запроса должно быть установлено выше (т. е. 30 секунд). Если вам требуется время ожидания, превышающее 30 секунд, отправьте нам сообщение по адресу secret-scanning@github.com.