Skip to main content

Secret scanningパートナープログラム

サービス プロバイダーは、 GitHub と連携して、シークレット トークン形式をシークレット スキャンによってセキュリティで保護することができます。シークレット形式の誤ったコミットを検索し、サービス プロバイダーの検証エンドポイントに送信できます。

この機能を使用できるユーザーについて

パートナーに対するシークレット スキャンニング アラート は、既定で次のリポジトリで実行されます:

  • GitHub のパブリック リポジトリとパブリック npm パッケージ

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 メッセージ ペイロードを含む Webhook を受信するシークレット アラート サービスを作成します。
  4. シークレットアラートサービスに、署名検証を実装してください。
  5. シークレットアラートサービスに、シークレットの破棄とユーザへの通知を実装してください。
  6. 誤検知に対するフィードバックを行ないます (任意)。

プロセスを開始するには、 GitHub にお問い合わせください

登録プロセスを開始するには、[email protected] にメールしてください。

secret scanningプログラムの詳細を受け取り、続行する前にGitHubの参加条件に同意する必要があります。

シークレットの特定と正規表現の作成

シークレットをスキャンするには、GitHubsecret scanning プログラムに含めるシークレットごとに次の情報が必要です。

  • シークレットの種類に対する、一意かつ人が読める名前。 これを使用して、後でメッセージ ペイロードに Type 値を生成します。

  • このシークレットの種類を見つける正規表現。 できるかぎり正確さを心がけることをお勧めします。そうすることで、誤検知の数を減らすことができます。 高品質で識別可能なシークレットのベスト プラクティスは次のとおりです:

    • 一意に定義されたプレフィックス
    • エントロピの高いランダム文字列
    • 32 ビットのチェックサム

    プレフィックスと 32 ビットのチェックサムというシークレットの内訳を示すスクリーンショット。

  • サービスのテスト アカウント。 これにより、シークレットの例を生成して分析し、誤検知をさらに減らすことができます。

  • GitHubからメッセージを受信するエンドポイントの URL。 URL は各シークレットの種類ごとに一意である必要はありません。

この情報を [email protected] に送信します。

シークレットアラートサービスの作成

提供したURLに、パブリックでインターネットからアクセスできるHTTPエンドポイントを作成してください。 正規表現の一致がパブリックに見つかると、 GitHub はエンドポイントに HTTP POST メッセージを送信します。

要求本文の例

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

メッセージ本文は、1 つ以上のオブジェクトが含まれる JSON 配列であり、各オブジェクトは 1 つのシークレットの一致を表しています。 エンドポイントには、タイムアウトすることなく大量の一致を含むリクエストを処理する能力が必要とされます。シークレットの一致には、それぞれ次のキーがあります。

  • token: シークレットが一致する値。
  • type: 正規表現を識別するために指定した一意の名前。
  • url: 一致が見つかったパブリック URL (空の場合があります)
  • **ソース:**GitHubでトークンが見つかった場所。

source の有効値の一覧は次のとおりです。

  • Content
  • 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
  • 手動提出
  • Unknown

シークレット アラート サービスに署名検証を実装する

サービスへの HTTP 要求には、受信したメッセージが純粋に GitHubのものであり、悪意のあるものではないことを検証するために使用することを強くお勧めするヘッダーも含まれます。

確認するのは次の 2 つの HTTP ヘッダーです。

  • Github-Public-Key-Identifier: API からどの key_identifier を使うか
  • 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/"}]

次のコード スニペットは、署名の検証をどのように行うかについて示しています。 このコード例では、レート制限に達しないように、生成された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] にメールでお問い合わせください。

当社がシークレットを報告する際は、トークン、型識別子、コミットURLを含む各要素のJSON配列を送信します。 当社がフィードバックを受け取る際、あなたは検出されたトークンが正しい認証情報を持っているかいないかについての情報を送信します。 フィードバックは以下のフォーマットで受け取ります。

生のトークンは以下のように送信できます。

[
  {
    "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") のいずれか 1 つだけを送信してください。両方を送信しないでください。
  • 生のトークンをハッシュ化する場合、SHA-256のみを使用します。他のハッシュ化アルゴリズムは使用しないでください。
  • ラベルは、トークンが真陽性 ("true_positive") か誤検知 ("false_positive") かを示します。 これら 2 つの、小文字のリテラル文字列のみを受け付けます。

メモ

誤検知に関するデータを提供するパートナーに対しては、要求のタイムアウトは長め (30 秒) に設定されています。 30 秒を超えるタイムアウトが必要な場合は、[email protected] にメールでお問い合わせください。