Skip to main content

비밀 검사 파트너 프로그램

서비스 공급자는 GitHub와 협력하여 비밀 검색을 통해 비밀 토큰 형식을 보호하도록 할 수 있습니다. 그러면 커밋한 비밀 형식을 검색하고 서비스 공급자의 확인 엔드포인트로 보낼 수 있습니다.

누가 이 기능을 사용할 수 있나요?

파트너에 대한 비밀 검사 경고은(는) 다음 리포지토리에서 기본적으로 실행됩니다.

  • GitHub의 퍼블릭 리포지토리 및 공용 npm 패키지

이 문서의 내용

GitHub는 실수로 커밋된 자격 증명이 사기에 사용되지 않도록 알려진 비밀 형식이 있는지 리포지토리를 검색합니다. Secret scanning는 기본적으로 공용 리포지토리 및 공용 npm 패키지에서 발생합니다. 리포지토리 관리자와 조직 소유자는 프라이빗 리포리포지토리에서도 secret scanning를 활성화할 수 있습니다. 서비스 공급자는 GitHub과 협력하여 비밀 형식이 secret scanning에 포함되도록 할 수 있습니다.

공개 소스에 비밀 형식의 일치 항목이 있으면 페이로드가 선택한 HTTP 엔드포인트로 전송됩니다.

secret scanning에 대해 구성된 프라이빗 리포지토리에서 비밀 형식 일치 항목이 발견되면 리포지토리 관리자와 커밋한 사람이 알림을 받고 GitHub에서 secret scanning 결과를 보고 관리할 수 있습니다. 자세한 내용은 비밀 검사 경고 관리을(를) 참조하세요.

이 문서에서는 서비스 공급자로 GitHub와 협력하는 방법과 secret scanning 파트너 프로그램에 가입하는 방법을 설명합니다.

secret scanning 프로세스

다음 다이어그램은 서비스 공급자의 확인 엔드포인트로 전송된 모든 일치 항목과 함께 퍼블릭 리포지토리에 대한 secret scanning 프로세스가 요약되어 나와 있습니다. 유사한 프로세스는 npm 레지스트리의 공개 패키지에 노출된 서비스 공급자 토큰을 보냅니다.

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

GitHub에서 secret scanning 프로그램에 조인

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

GitHub에 문의하여 이 프로세스를 시작합니다.

등록 프로세스를 시작하려면 secret-scanning@github.com으로 이메일을 보내주세요.

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

비밀 식별 및 정규식 만들기

비밀을 검색하려면 GitHub에는 secret scanning 프로그램에 포함하려는 각 비밀에 대해 다음 정보가 필요합니다.

  • 사람이 읽을 수 있으며 비밀 형식에 대한 고유한 이름. 이 정보를 사용하여 나중에 메시지 페이로드에서 Type 값을 만들 수 있습니다.

  • 비밀 형식을 찾는 정규식. 가능한 정확도를 높이는 것이 좋습니다. 그러면 가양성을 줄이는 데 효과적입니다. 다음은 고품질의 식별 가능한 비밀에 대한 모범 사례입니다.

    • 고유하게 정의된 접두사
    • 높은 엔트로피 임의 문자열
    • 32비트 체크섬

    비밀을 접두사 및 32비트 체크섬으로 나누어 표시된 스크린샷.

  • 서비스의 테스트 계정. 이를 통해 비밀의 예제를 생성하고 분석함으로써 가양성을 더욱 줄일 수 있습니다.

  • 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 배열이며, 각 객체는 단일 비밀 일치 항목을 표시합니다. 엔드포인트는 다수의 일치 항목이 포함된 요청을 시간 초과 없이 처리할 수 있어야 합니다. 각 비밀 일치 항목의 키는 다음과 같습니다.

  •           **토큰:** 비밀 일치 항목의 값입니다.
    
  •           **형식:** 정규식을 식별하기 위해 제공한 고유한 이름입니다.
    
  •           **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가 퍼블픽 및 손상에 대한 메시지를 보내는 비밀을 고려하는 것이 좋습니다.

가양성에 대한 피드백 제공

파트너 응답에서 검색된 개별 비밀의 유효성에 대한 피드백을 수집합니다. 참여하려면 secret-scanning@github.com으로 이메일을 보내주세요.

귀하에게 비밀을 보고할 때 토큰, 형식 식별자 및 커밋 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초보다 더 긴 시간 제한이 필요한 경우 secret-scanning@github.com으로 이메일을 보내주세요.