GitHub сканирует репозитории известных форматов секретов, чтобы предотвратить случайное использование учетных данных, которые были зафиксированы случайно. Secret scanning по умолчанию выполняется в общедоступных репозиториях и может быть активировано в частных репозиториях администраторами репозитория или владельцами организации. Как поставщик услуг вы можете сотрудничать с GitHub, чтобы форматы секретов были включены в наше secret scanning.
При обнаружении соответствия формата секрета в общедоступном репозитории полезные данные передаются в конечную точку HTTP на ваше усмотрение.
При обнаружении соответствия формата секрета в частном репозитории, настроенном для secret scanning, об этом оповещаются администраторы репозитория и средство фиксации, которые могут просматривать результат secret scanning в GitHub. Дополнительные сведения см. в разделе Управление оповещениями о проверке секретов.
В этой статье описывается способ сотрудничества с GitHub в качестве поставщика услуг и присоединения к партнерской программе secret scanning.
Процесс secret scanning
На следующей схеме показан процесс secret scanning для общедоступных репозиториев с любыми совпадениями, отправленными в конечную точку проверки поставщика услуг.
Присоединение программы secret scanning на GitHub
- Чтобы начать процесс, обратитесь к GitHub.
- Определите соответствующие секреты, которые необходимо сканировать, и создайте регулярные выражения для их записи.
- Для совпадений секретов, найденных в общедоступных репозиториях, создайте службу оповещений о секретах, которая принимает веб-перехватчики из GitHub, где содержатся полезные данные сообщения secret scanning.
- Реализуйте проверку подписи в службе оповещений о секретах.
- Реализуйте отзыв секретов и уведомление пользователя в службе оповещений о секретах.
- Предоставьте отзыв о ложноположительных результатах (необязательно).
Чтобы начать процесс, обратитесь к 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, содержащий один или несколько объектов, каждый из которых представляет одно совпадение секретов. Конечная точка должна обрабатывать запросы с большим количеством совпадений без истечения времени ожидания. Ключи для каждого совпадения секретов:
- token: значение совпадения секрета.
- type: уникальное имя, предоставленное для идентификации регулярного выражения.
- url: общедоступный URL-адрес, по которому было найдено совпадение (может быть пустым).
- source: расположение маркера в GitHub.
Список допустимых значений для source
:
- содержимое
- фиксация
- pull_request_description
- pull_request_comment
- issue_description
- issue_comment
- discussion_body
- discussion_comment
- commit_comment
- gist_content
- gist_comment
- неизвестно
Реализуйте проверку подписи в службе оповещений о секретах
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
.
Примечание. При отправке запроса в конечную точку вышеупомянутого открытого ключа можно достигнуть ограничений скорости. Чтобы избежать превышения ограничений скорости, можно использовать personal access token (classic) (области не требуются) или fine-grained personal access token (требуется только автоматический доступ на чтение к общедоступным репозиториям), как показано в примерах ниже, или использовать условный запрос. Дополнительные сведения см. в разделе Начало работы с REST API.
Примечание: подпись была создана с помощью необработанного текста сообщения. Поэтому так важно для проверки подписи использовать необработанный текст сообщения, а не синтаксический анализ и преобразование JSON в строку, чтобы избежать изменения содержимого сообщения или изменения интервала.
Пример HTTP POST, отправленный в конечную точку проверки
POST / HTTP/2
Host: HOST
Accept: */*
content-type: application/json
GITHUB-PUBLIC-KEY-IDENTIFIER: f9525bf080f75b3506ca1ead061add62b8633a346606dc5fe544e29231c6ee0d
GITHUB-PUBLIC-KEY-SIGNATURE: MEUCIFLZzeK++IhS+y276SRk2Pe5LfDrfvTXu6iwKKcFGCrvAiEAhHN2kDOhy2I6eGkOFmxNkOJ+L2y8oQ9A2T9GGJo6WJY=
Content-Length: 83
[{"token":"some_token","type":"some_type","url":"some_url","source":"some_source"}]
Примечание. Идентификатор ключа и подпись из примера полезных данных являются производными от тестового ключа. Открытый ключ для них:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsz9ugWDj5jK5ELBK42ynytbo38gP
HzZFI03Exwz8Lh/tCfL3YxwMdLjB+bMznsanlhK0RwcGP3IDb34kQDIo3Q==
-----END PUBLIC KEY-----
В следующих фрагментах кода показано, как можно выполнить проверку подписи.
В примерах кода предполагается, что вы задали переменную среды с помощью 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.