{"meta":{"title":"密码扫描合作伙伴计划","intro":"作为服务提供者，你可以与 GitHub 合作，通过密码扫描保护你的密码令牌格式，该扫描将搜索意外提交的密码格式，并且可以发送到服务提供者的验证端点。","product":"安全性和代码质量","breadcrumbs":[{"href":"/zh/code-security","title":"安全性和代码质量"},{"href":"/zh/code-security/tutorials","title":"Tutorials"},{"href":"/zh/code-security/tutorials/secret-scanning-partner-program","title":"合作伙伴计划"}],"documentType":"article"},"body":"# 密码扫描合作伙伴计划\n\n作为服务提供者，你可以与 GitHub 合作，通过密码扫描保护你的密码令牌格式，该扫描将搜索意外提交的密码格式，并且可以发送到服务提供者的验证端点。\n\nGitHub 扫描仓库查找已知的密码格式，以防止欺诈性使用意外提交的凭据。 Secret scanning 默认发生在公共存储库和公共 npm 包上。 存储库管理员和组织所有者还可以在专用存储库上启用 secret scanning。 作为服务提供者，你可以与 GitHub 合作，让你的密码格式包含在我们的 secret scanning 中。\n\n在公共源中找到密码格式的匹配项时，将向你选择的 HTTP 终结点发送有效负载。\n\n在为 secret scanning 配置的专用仓库中找到密码格式的匹配项时，仓库管理员和提交者将收到警报，并且可以查看和管理 GitHub 上的 secret scanning 结果。 有关详细信息，请参阅“[管理机密扫描警报](/zh/code-security/secret-scanning/managing-alerts-from-secret-scanning)”。\n\n本文介绍作为服务提供者如何与 GitHub 合作并加入 secret scanning 合作伙伴计划。\n\n## secret scanning 流程\n\n下图总结了在公共仓库中进行 secret scanning 并将任何匹配项发送到服务提供者的验证端点的流程。 类似的过程会发送在 npm 注册表上的公共包中公开的服务提供程序令牌。\n\n![显示扫描密码并向服务提供者的验证终结点发送匹配项的关系图。](/assets/images/help/security/secret-scanning-flow.png)\n\n## 在 GitHub 上加入 secret scanning 计划\n\n1. 联系 GitHub 以启动流程。\n2. 识别要扫描的相关密码，并创建正则表达式来捕获它们。 有关详细信息和建议，请参阅下面的[识别你的机密并创建正则表达式](#identify-your-secrets-and-create-regular-expressions)。\n3. 对于公开发现的机密匹配项，创建机密警报服务，该服务接受来自 GitHub 的 Webhook，其中包含 secret scanning 消息负载。\n4. 在密码警报服务中实施签名验证。\n5. 在密码警报服务中实施密码撤销和用户通知。\n6. 提供误报的反馈（可选）。\n\n### 联系 GitHub 以启动流程\n\n要启动注册流程，请发送电子邮件至 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>。\n\n你将收到有关 secret scanning 计划的详细信息，你需要同意 GitHub 的参与条款才能继续。\n\n### 识别你的密码并创建正则表达式\n\n要扫描你的密码，GitHub 需要你要包含在 secret scanning 计划中的每个密码的以下信息：\n\n* 密码类型的唯一、人类可读的名称。 稍后我们将使用它在消息有效负载中生成 `Type` 值。\n\n* 查找密码类型的正则表达式。 建议尽可能精确，因为这样有助于减少误报的数量。 有关高质量、可识别机密的一些最佳做法包括：\n\n  * 唯一定义的前缀\n  * 高熵随机字符串\n  * 32 位校验和\n\n  ![显示机密分解为前缀和 32 位校验和的屏幕截图。](/assets/images/help/security/regular-expression-guidance.png)\n\n* 服务的测试帐户。 这将使我们能够生成和分析机密的示例，进一步减少误报。\n\n* 从 GitHub 接收消息的端点的 URL。 对于每个机密类型，URL 不必是唯一的。\n\n将此信息发送至 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>。\n\n### 创建密码警报服务\n\n在你提供给我们的 URL 上创建一个可访问 Internet 的公共 HTTP 端点。 公开发现正则表达式的匹配项时，GitHub 将向你的终结点发送 HTTP `POST` 消息。\n\n#### 示例请求正文\n\n```json\n[\n  {\n    \"token\":\"NMIfyYncKcRALEXAMPLE\",\n    \"type\":\"mycompany_api_token\",\n    \"url\":\"https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt\",\n    \"source\":\"content\"\n  }\n]\n```\n\n消息正文是一个 JSON 数组，其中包含一个或多个对象，每个对象表示一个机密匹配项。 你的终结点应该能够在不超时的情况下处理包含大量匹配项的请求。每个机密匹配项的密钥为：\n\n* **token：** 机密匹配项的值。\n\n* **type：** 你提供的用于标识正则表达式的唯一名称。\n\n* **url：** 发现匹配项的公共 URL（可能为空）\n\n* **source：** 在 GitHub 上发现该令牌的位置。\n\n  ```\n          `source` 的有效值列表如下：\n  ```\n\n* Content\n\n* 提交\n\n* Pull\\_request\\_title\n\n* Pull\\_request\\_description\n\n* Pull\\_request\\_comment\n\n* Issue\\_title\n\n* Issue\\_description\n\n* Issue\\_comment\n\n* Discussion\\_title\n\n* Discussion\\_body\n\n* Discussion\\_comment\n\n* Commit\\_comment\n\n* Gist\\_content\n\n* Gist\\_comment\n\n* 维基内容\n\n* Wiki\\_commit\n\n* Npm\n\n* 手动提交\n\n* 未知\n\n### 在密码警报服务中实施签名验证\n\n向服务发出的 HTTP 请求还将包含标头，我们强烈建议使用这些标头来验证收到的消息是否真正来自 GitHub，并且不是恶意消息。\n\n要查找的两个 HTTP 标头为：\n\n* `Github-Public-Key-Identifier`：要从我们的 API 中使用哪个 `key_identifier`\n* `Github-Public-Key-Signature`：负载的签名\n\n可以从 <https://api.github.com/meta/public_keys/secret_scanning> 检索 GitHub 机密扫描公钥并使用 `ECDSA-NIST-P256V1-SHA256` 算法验证消息。 终结点将提供多个 `key_identifier` 和公钥。 可以根据 `Github-Public-Key-Identifier` 的值确定使用哪个公钥。\n\n> \\[!NOTE]\n> 向上述公钥终结点发送请求时，可能会遇到速率限制。 为避免遇到速率限制，可按照下面示例中的建议，使用 personal access token (classic)（无需作用域）或 fine-grained personal access token（仅需自动公共存储库读取权限），或使用条件请求。 有关详细信息，请参阅“[REST API 入门](/zh/rest/guides/getting-started-with-the-rest-api#conditional-requests)”。\n\n> \\[!NOTE]\n> 签名是使用原始消息正文生成的。 因此，你也必须使用原始消息正文进行签名验证，而不是解析和串联 JSON，以避免重新排列消息或更改间距，这一点很重要。\n\n```\n          **发送到验证终结点的 HTTP POST 示例**\n```\n\n```http\nPOST / HTTP/2\nHost: HOST\nAccept: */*\nContent-Length: 104\nContent-Type: application/json\nGithub-Public-Key-Identifier: bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c\nGithub-Public-Key-Signature: MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg==\n\n[{\"source\":\"commit\",\"token\":\"some_token\",\"type\":\"some_type\",\"url\":\"https://example.com/base-repo-url/\"}]\n```\n\n以下代码片段演示如何执行签名验证。\n代码示例假定已使用生成的 `GITHUB_PRODUCTION_TOKEN` 设置名为 [](https://github.com/settings/tokens) 的环境变量以避免达到速率限制。 personal access token 不需要任何作用域/权限。\n\n```\n          **Go 语言验证示例**\n```\n\n```golang\npackage main\n\nimport (\n  \"crypto/ecdsa\"\n  \"crypto/sha256\"\n  \"crypto/x509\"\n  \"encoding/asn1\"\n  \"encoding/base64\"\n  \"encoding/json\"\n  \"encoding/pem\"\n  \"errors\"\n  \"fmt\"\n  \"math/big\"\n  \"net/http\"\n  \"os\"\n)\n\nfunc main() {\n  payload := `[{\"source\":\"commit\",\"token\":\"some_token\",\"type\":\"some_type\",\"url\":\"https://example.com/base-repo-url/\"}]`\n\n  kID := \"bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c\"\n\n  kSig := \"MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg==\"\n\n  // Fetch the list of GitHub Public Keys\n  req, err := http.NewRequest(\"GET\", \"https://api.github.com/meta/public_keys/secret_scanning\", nil)\n  if err != nil {\n    fmt.Printf(\"Error preparing request: %s\\n\", err)\n    os.Exit(1)\n  }\n\n  if len(os.Getenv(\"GITHUB_PRODUCTION_TOKEN\")) == 0 {\n    fmt.Println(\"Need to define environment variable GITHUB_PRODUCTION_TOKEN\")\n    os.Exit(1)\n  }\n\n  req.Header.Add(\"Authorization\", \"Bearer \"+os.Getenv(\"GITHUB_PRODUCTION_TOKEN\"))\n\n  resp, err := http.DefaultClient.Do(req)\n  if err != nil {\n    fmt.Printf(\"Error requesting GitHub signing keys: %s\\n\", err)\n    os.Exit(2)\n  }\n\n  decoder := json.NewDecoder(resp.Body)\n  var keys GitHubSigningKeys\n  if err := decoder.Decode(&keys); err != nil {\n    fmt.Printf(\"Error decoding GitHub signing key request: %s\\n\", err)\n    os.Exit(3)\n  }\n\n  // Find the Key used to sign our webhook\n  pubKey, err := func() (string, error) {\n    for _, v := range keys.PublicKeys {\n      if v.KeyIdentifier == kID {\n        return v.Key, nil\n\n      }\n    }\n    return \"\", errors.New(\"specified key was not found in GitHub key list\")\n  }()\n\n  if err != nil {\n    fmt.Printf(\"Error finding GitHub signing key: %s\\n\", err)\n    os.Exit(4)\n  }\n\n  // Decode the Public Key\n  block, _ := pem.Decode([]byte(pubKey))\n  if block == nil {\n    fmt.Println(\"Error parsing PEM block with GitHub public key\")\n    os.Exit(5)\n  }\n\n  // Create our ECDSA Public Key\n  key, err := x509.ParsePKIXPublicKey(block.Bytes)\n  if err != nil {\n    fmt.Printf(\"Error parsing DER encoded public key: %s\\n\", err)\n    os.Exit(6)\n  }\n\n  // Because of documentation, we know it's a *ecdsa.PublicKey\n  ecdsaKey, ok := key.(*ecdsa.PublicKey)\n  if !ok {\n    fmt.Println(\"GitHub key was not ECDSA, what are they doing?!\")\n    os.Exit(7)\n  }\n\n  // Parse the Webhook Signature\n  parsedSig := asn1Signature{}\n  asnSig, err := base64.StdEncoding.DecodeString(kSig)\n  if err != nil {\n    fmt.Printf(\"unable to base64 decode signature: %s\\n\", err)\n    os.Exit(8)\n  }\n  rest, err := asn1.Unmarshal(asnSig, &parsedSig)\n  if err != nil || len(rest) != 0 {\n    fmt.Printf(\"Error unmarshalling asn.1 signature: %s\\n\", err)\n    os.Exit(9)\n  }\n\n  // Verify the SHA256 encoded payload against the signature with GitHub's Key\n  digest := sha256.Sum256([]byte(payload))\n  keyOk := ecdsa.Verify(ecdsaKey, digest[:], parsedSig.R, parsedSig.S)\n\n  if keyOk {\n    fmt.Println(\"THE PAYLOAD IS GOOD!!\")\n  } else {\n    fmt.Println(\"the payload is invalid :(\")\n    os.Exit(10)\n  }\n}\n\ntype GitHubSigningKeys struct {\n  PublicKeys []struct {\n    KeyIdentifier string `json:\"key_identifier\"`\n    Key           string `json:\"key\"`\n    IsCurrent     bool   `json:\"is_current\"`\n  } `json:\"public_keys\"`\n}\n\n// asn1Signature is a struct for ASN.1 serializing/parsing signatures.\ntype asn1Signature struct {\n  R *big.Int\n  S *big.Int\n}\n```\n\n```\n          **Ruby 语言验证示例**\n```\n\n```ruby\nrequire 'openssl'\nrequire 'net/http'\nrequire 'uri'\nrequire 'json'\nrequire 'base64'\n\npayload = <<-EOL\n[{\"source\":\"commit\",\"token\":\"some_token\",\"type\":\"some_type\",\"url\":\"https://example.com/base-repo-url/\"}]\nEOL\n\npayload = payload\n\nsignature = \"MEQCIQDaMKqrGnE27S0kgMrEK0eYBmyG0LeZismAEz/BgZyt7AIfXt9fErtRS4XaeSt/AO1RtBY66YcAdjxji410VQV4xg==\"\n\nkey_id = \"bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c\"\n\nurl = URI.parse('https://api.github.com/meta/public_keys/secret_scanning')\n\nraise \"Need to define GITHUB_PRODUCTION_TOKEN environment variable\" unless ENV['GITHUB_PRODUCTION_TOKEN']\nrequest = Net::HTTP::Get.new(url.path)\nrequest['Authorization'] = \"Bearer #{ENV['GITHUB_PRODUCTION_TOKEN']}\"\n\nhttp = Net::HTTP.new(url.host, url.port)\nhttp.use_ssl = (url.scheme == \"https\")\n\nresponse = http.request(request)\n\nparsed_response = JSON.parse(response.body)\n\ncurrent_key_object = parsed_response[\"public_keys\"].find { |key| key[\"key_identifier\"] == key_id }\n\ncurrent_key = current_key_object[\"key\"]\n\nopenssl_key = OpenSSL::PKey::EC.new(current_key)\n\nputs openssl_key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), payload.chomp)\n```\n\n```\n          **JavaScript 语言验证示例**\n```\n\n```javascript\nconst crypto = require(\"crypto\");\nconst axios = require(\"axios\");\n\nconst GITHUB_KEYS_URI = \"https://api.github.com/meta/public_keys/secret_scanning\";\n\n/**\n * Verify a payload and signature against a public key\n * @param {String} payload the value to verify\n * @param {String} signature the expected value\n * @param {String} keyID the id of the key used to generated the signature\n * @return {void} throws if the signature is invalid\n */\nconst verify_signature = async (payload, signature, keyID) => {\n  if (typeof payload !== \"string\" || payload.length === 0) {\n    throw new Error(\"Invalid payload\");\n  }\n  if (typeof signature !== \"string\" || signature.length === 0) {\n    throw new Error(\"Invalid signature\");\n  }\n  if (typeof keyID !== \"string\" || keyID.length === 0) {\n    throw new Error(\"Invalid keyID\");\n  }\n\n  const keys = (await axios.get(GITHUB_KEYS_URI)).data;\n  if (!(keys?.public_keys instanceof Array) || keys.length === 0) {\n    throw new Error(\"No public keys found\");\n  }\n\n  const publicKey = keys.public_keys.find((k) => k.key_identifier === keyID) ?? null;\n  if (publicKey === null) {\n    throw new Error(\"No public key found matching key identifier\");\n  }\n\n  const verify = crypto.createVerify(\"SHA256\").update(payload);\n  if (!verify.verify(publicKey.key, Buffer.from(signature, \"base64\"), \"base64\")) {\n    throw new Error(\"Signature does not match payload\");\n  }\n};\n```\n\n### 在密码警报服务中实施密码撤销和用户通知\n\n针对公开发现的 secret scanning，你可以增强密码警报服务，以撤销泄露的密码并通知受影响的用户。 如何在密码警报服务中实现此功能取决于你，但我们建议你考虑 GitHub向你发送的公开和泄露示警消息所涉及的任何密码。\n\n### 提供误报的反馈\n\n我们在合作伙伴响应中收集有关检测到的各个密码有效性的反馈。 如果希望参与，请发送电子邮件至 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>。\n\n向你报告密码时，我们会发送一个 JSON 数组，其中有包含令牌、类型标识符和提交 URL 的每个元素。 当你向我们发送反馈时，你将向我们发送有关检测到的令牌是真凭据还是假凭据的信息。 我们接受以下格式的反馈。\n\n你可以向我们发送原始令牌：\n\n```json\n[\n  {\n    \"token_raw\": \"The raw token\",\n    \"token_type\": \"ACompany_API_token\",\n    \"label\": \"true_positive\"\n  }\n]\n```\n\n你还可以使用 SHA-256 对原始令牌执行单向加密哈希后，以哈希形式提供令牌：\n\n```json\n[\n  {\n    \"token_hash\": \"The SHA-256 hashed form of the raw token\",\n    \"token_type\": \"ACompany_API_token\",\n    \"label\": \"false_positive\"\n  }\n]\n```\n\n重要事项：\n\n* 你应该只向我们发送令牌的原始形式 (\"token\\_raw\") 或哈希形式，而不要同时发送这两种形式。\n* 对于原始令牌的哈希形式，你只能使用 SHA-256 对令牌进行哈希处理，而不能使用任何其他哈希算法。\n* 用标签指示令牌为实报 (\"true\\_positive\") 还是误报 (\"false\\_positive\")。 只允许使用这两个小写的文字字符串。\n\n> \\[!NOTE]\n> 对于提供误报数据的合作伙伴，我们的请求超时设置为更长时间（即 30 秒）。 如果需要超过 30 秒的超时时间，请发送电子邮件至 <a href=\"mailto:secret-scanning@github.com\"><secret-scanning@github.com></a>。"}