Skip to main content

비밀 검사 파트너 프로그램

서비스 공급자는 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 프로세스를 요약하며, 일치하는 항목은 서비스 공급자의 verify 엔드포인트로 전송됩니다. 유사한 프로세스는 npm 레지스트리의 공개 패키지에 노출된 서비스 공급자 토큰을 보냅니다.

비밀을 검색하고 서비스 공급자의 확인 엔드포인트에 일치 항목을 보내는 프로세스를 보여주는 다이어그램.

secret scanning에서 GitHub 프로그램에 참여하기

  1. 프로세스를 시작하려면 문의 GitHub 하세요.
  2. 검색하려는 관련 비밀을 식별하고 해당 비밀을 캡처하는 정규식을 만듭니다. 자세한 내용 및 권장 사항은 아래의 비밀 식별 및 정규식 만들기를 참조하세요.
  3. 공개적으로 발견된 시크릿 일치 항목의 경우, GitHub에서 secret scanning 메시지 페이로드가 포함된 웹훅을 수신하는 시크릿 경고 서비스를 만드세요.
  4. 비밀 알림 서비스에서 서명 확인을 구현합니다.
  5. 비밀 알림 서비스에서 비밀 해지 및 사용자 알림을 구현합니다.
  6. 가양성(선택 사항)에 대한 피드백을 제공합니다.

프로세스를 시작하려면 연락처로 문의 GitHub 하세요.

등록 프로세스를 시작하려면 [email protected]으로 이메일을 보내주세요.

프로그램에 대한 secret scanning 세부 정보를 받게 되며 계속하기 전에 참여 조건에 동의 GitHub해야 합니다.

비밀 식별 및 정규식 만들기

GitHub에서 비밀 정보를 검색하려면 secret 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"
  }
]

메시지 본문은 하나 이상의 객체가 포함된 JSON 배열이며, 각 객체는 단일 비밀 일치 항목을 표시합니다. 엔드포인트는 다수의 일치 항목이 포함된 요청을 시간 초과 없이 처리할 수 있어야 합니다. 각 비밀 일치 항목의 키는 다음과 같습니다.

  • 토큰: 비밀 일치 항목의 값입니다.
  • 형식: 정규식을 식별하기 위해 제공한 고유한 이름입니다.
  • 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
  • 수동 제출
  • 알 수 없음

비밀 알림 서비스에서 서명 확인을 구현합니다.

서비스에 대한 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 값에 따라 사용할 공개 키를 결정할 수 있습니다.

참고

위의 공개 키 엔드포인트에 요청을 보내는 경우 속도 제한에 도달할 수 있습니다. 속도 제한에 도달하지 않도록 하려면 아래 샘플에서 제안된 대로 (범위 필요 없음) 또는 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를 사용해 GITHUB_PRODUCTION_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”)만 보내야 하지만 둘 다 보내지는 않습니다.
  • 원시 토큰의 해시된 형식의 경우 SHA-256만 사용하여 다른 해시 알고리즘이 아닌 토큰을 해시할 수 있습니다.
  • 레이블은 토큰이 진양성(“true_positive”) 또는 가양성(“false_positive”)인지 여부를 나타냅니다. 이러한 소문자 리터럴 문자열 두 개만 허용됩니다.

참고

가양성에 대한 데이터를 제공하는 파트너에게는 더 길어진(30초) 요청 시간 제한이 적용됩니다. 30초보다 더 긴 시간 제한이 필요한 경우 [email protected]으로 이메일을 보내주세요.