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
- Чтобы начать процесс, обратитесь к GitHub.
- Определите соответствующие секреты, которые необходимо сканировать, и создайте регулярные выражения для их записи. Дополнительные сведения и рекомендации см. в разделе "Определение секретов и создание регулярных выражений" ниже.
- Для открытых совпадений секретов создайте службу предупреждений секрета, которая принимает веб-перехватчики из GitHub с полезными данными secret scanning.
- Реализуйте проверку подписи в службе оповещений о секретах.
- Реализуйте отзыв секретов и уведомление пользователя в службе оповещений о секретах.
- Предоставьте отзыв о ложноположительных результатах (необязательно).
Чтобы начать процесс, обратитесь к GitHub
Чтобы начать процесс регистрации, отправьте сообщение электронной почты [email protected].
Вы получите сведения о программе secret scanning, и перед продолжением необходимо согласиться с условиями участия GitHub.
Определение секретов и создание регулярных выражений
Для сканирования секретов GitHub требуются следующие фрагменты информации для каждого секрета, который требуется включить в программу secret scanning:
-
Уникальное, удобочитаемое пользователем имя для типа секрета. Мы будем использовать его позже для создания значения
Type
в полезных данных сообщения. -
Регулярное выражение, которое позволяет найти тип секрета. Рекомендуется максимально точно, так как это поможет сократить количество ложных срабатываний. Ниже приведены некоторые рекомендации по обеспечению высокого качества идентифицируемых секретов:
- Уникально определенный префикс
- Случайные строки высокой энтропии
- 32-разрядная контрольная сумма
-
Тестовая учетная запись для службы. Это позволит нам создавать и анализировать примеры секретов, уменьшая ложные срабатывания.
-
URL-адрес конечной точки, получающей сообщения от GitHub. URL-адрес не должен быть уникальным для каждого типа секрета.
Отправьте эти сведения в [email protected].
Создание службы оповещений о секрете
Создайте общедоступную в Интернете конечную точку 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
:
- Содержимое
- 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
- Неизвестно
Реализуйте проверку подписи в службе оповещений о секретах
HTTP-запрос к вашей службе также будет содержать заголовки, которые мы настоятельно рекомендуем использовать для проверки получаемых сообщений из GitHub, и не являются вредоносными.
Два заголовка HTTP для поиска:
Github-Public-Key-Identifier
: чтоkey_identifier
следует использовать из нашего APIGithub-Public-Key-Signature
: подпись полезных данных
Открытый ключ сканирования секрета GitHub можно получить из https://api.github.com/meta/public_keys/secret_scanning и проверить сообщение с помощью алгоритма ECDSA-NIST-P256V1-SHA256
. Конечная точка предоставляет несколько key_identifier
и открытых ключей. Вы можете определить, какой открытый ключ следует использовать на основе значения Github-Public-Key-Identifier
.
Note
При отправке запроса в конечную точку открытого ключа выше можно ударить по ограничениям скорости. Чтобы избежать ограничений скорости, можно использовать personal access token (classic) (без областей) или fine-grained personal access token (только автоматический доступ на чтение общедоступных репозиториев), как показано в приведенных ниже примерах, или использовать условный запрос. Дополнительные сведения см. в разделе Начало работы с REST API.
Note
Подпись была создана с помощью текста необработанного сообщения. Поэтому так важно для проверки подписи использовать необработанный текст сообщения, а не синтаксический анализ и преобразование 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 := `[{"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
}
Пример проверки в 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)
Пример проверки в 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 отправляет вам сообщения как общедоступные и скомпрометированные.
Отправка отзыва о ложноположительных результатах
Мы собираем отзывы о допустимости обнаруженных отдельных секретах в ответах партнера. Если вы хотите принять участие в этом, отправьте нам письмо по адресу [email protected].
При передаче вам секретов мы отправляем массив 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"). Допускаются только эти две строки литерала в нижнем регистре.
Note
Время ожидания запроса должно быть выше (т. е. 30 секунд) для партнеров, которые предоставляют данные о ложных срабатываниях. Если вам требуется время ожидания, превышающее 30 секунд, отправьте нам сообщение по адресу [email protected].