跳至主要内容

密钥扫描合作伙伴计划

作为服务提供商,您可以与 GitHub 合作,将您的机密令牌格式纳入机密扫描,以检测意外提交的机密格式,并可将结果发送到服务提供商的验证端点。

谁可以使用此功能?

合作伙伴的机密扫描警报默认在以下仓库中运行

  • GitHub 上的公共仓库和公共 npm 包

GitHub 会扫描仓库以查找已知机密格式,从而防止因意外提交导致的凭证被欺诈性使用。机密扫描默认在公共仓库和公共 npm 包上运行。仓库管理员和组织所有者也可以在私有仓库上启用机密扫描。作为服务提供商,您可以与 GitHub 合作,使您的机密格式包含在我们的机密扫描中。

当在公共来源中发现您机密格式的匹配项时,系统会向您指定的 HTTP 端点发送有效负载。

当在已配置机密扫描的私有仓库中发现您机密格式的匹配项时,仓库管理员和提交者会收到警报,并可在 GitHub 上查看和管理机密扫描结果。更多信息,请参阅 管理机密扫描警报

本文档说明了如何作为服务提供商与 GitHub 合作并加入机密扫描合作伙伴计划。

机密扫描流程

下图概括了公共仓库的机密扫描流程,匹配项会发送至服务提供商的验证端点。类似的流程也会将公开的 npm 包中暴露的令牌发送给服务提供商。

Diagram showing the process of scanning for a secret and sending matches to a service provider's verify endpoint.

加入 GitHub 的机密扫描计划

  1. 联系 GitHub 开始该流程。
  2. 确定您希望扫描的相关机密,并创建用于捕获它们的正则表达式。有关更详细的信息和建议,请参阅下面的 确定机密并创建正则表达式
  3. 对于公开发现的机密匹配,创建一个机密警报服务以接收来自 GitHub 的包含机密扫描消息负载的 webhook。
  4. 在您的机密警报服务中实现签名验证。
  5. 在您的机密警报服务中实现机密撤销和用户通知。
  6. 提供误报反馈(可选)。

联系 GitHub 开始该流程

要启动入驻流程,请发送电子邮件至 secret-scanning@github.com

您将收到有关机密扫描计划的详细信息,并需要在继续之前同意 GitHub 的参与条款。

确定机密并创建正则表达式

要对您的机密进行扫描,GitHub 需要您为每个希望纳入机密扫描计划的机密提供以下信息

  • 机密类型的唯一、易读名称。我们将在稍后的消息负载中使用它生成 Type 值。

  • 用于匹配该机密类型的正则表达式。我们建议您尽可能精确,因为这有助于降低误报数量。高质量、可识别机密的最佳实践包括

    • 唯一定义的前缀
    • 高熵随机字符串
    • 32 位校验和

    Screenshot showing the breakdown of a secret into a prefix and a 32-bit checksum.

  • 您的服务的测试账号。这将使我们能够生成并分析机密示例,进一步降低误报。

  • 接收 GitHub 消息的端点 URL。该 URL 不必为每种机密类型单独唯一。

将以上信息发送至 secret-scanning@github.com

创建机密警报服务

在您提供给我们的 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"
  }
]

消息体是一个 JSON 数组,包含一个或多个对象,每个对象代表一次机密匹配。您的端点应能够在不超时的情况下处理包含大量匹配的请求。每个机密匹配的键如下

  • token: 匹配到的机密值。
  • type: 您用于标识正则表达式的唯一名称。
  • url: 发现匹配的公共 URL(可能为空)。
  • source: 在 GitHub 上发现该令牌的位置。

source 的有效取值包括

  • 内容
  • 提交
  • 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
  • Wiki_content
  • Wiki_commit
  • Npm
  • Manual_submission
  • Unknown

在您的机密警报服务中实现签名验证

发送到您服务的 HTTP 请求还会包含我们强烈建议使用的头部,用于验证您收到的消息确实来自 GitHub,而非恶意来源。

需检查的两个 HTTP 头部是

  • Github-Public-Key-Identifier:使用我们 API 中哪个 key_identifier
  • Github-Public-Key-Signature:负载的签名。

您可以从 https://api.github.com/meta/public_keys/secret_scanning 获取 GitHub 机密扫描公钥,并使用 ECDSA-NIST-P256V1-SHA256 算法进行验证。该端点会返回多个 key_identifier 与对应的公钥。您可根据 Github-Public-Key-Identifier 的取值选择使用哪把公钥。

注意

当您请求上述公钥端点时可能会触发速率限制。为避免速率限制,您可以使用不需要任何权限的个人访问令牌(Classic)或仅需自动公共仓库读取权限的细粒度个人访问令牌(如示例所示),或使用条件请求。更多信息,请参阅 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 的环境变量设置为已生成的 个人访问令牌,以避免触发速率限制。该访问令牌不需要任何作用域/权限。

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");
  }
};

在您的机密警报服务中实现机密撤销和用户通知

对于公开发现的机密扫描,您可以在机密警报服务中添加撤销已暴露机密并通知受影响用户的功能。具体实现方式由您自行决定,但我们建议您将 GitHub 发送的所有机密视为已公开且已受损。

提供误报反馈

我们会在合作伙伴响应中收集对检测到的单个机密有效性的反馈。如果您愿意参与,请发送电子邮件至 secret-scanning@github.com

当我们向您报告机密时,会发送一个 JSON 数组,每个元素包含 token、type 标识符和提交 URL。您在向我们反馈时,需要提供该检测到的 token 是真实凭证还是误报。我们接受以下格式的反馈。

您可以直接发送原始 token

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

也可以在对原始 token 进行一次性 SHA-256 加密哈希后发送其哈希值

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

几个重要注意点

  • 您只能发送原始 token("token_raw")或哈希 token("token_hash") 中的一种,不能两者同时发送。
  • 对于原始 token 的哈希形式,只能使用 SHA-256 进行哈希,禁止使用其他哈希算法。
  • label 用于指示 token 是真实正例("true_positive")还是误报("false_positive")。仅允许这两个全小写的字面字符串。

注意

对于提供误报数据的合作伙伴,我们的请求超时时间已提升至 30 秒。如果您需要更长的超时时间,请发送电子邮件至 secret-scanning@github.com

© . This site is unofficial and not affiliated with GitHub, Inc.