웹 크롤러 인증 (WBA) 프로토콜을 사용하면 HTTP 메시지 서명 (RFC 9421)을 사용하여 에이전트 또는 크롤러의 ID를 암호화 방식으로 증명할 수 있습니다.
발신 요청에 서명하면 Google Cloud Fraud Defense (reCAPTCHA의 발전된 형태)와 같은 서비스에서 IP 주소나 User-Agent 문자열에만 의존하지 않고 에이전트를 직접 확인할 수 있습니다.
WBA로 에이전트를 인증하려면 다음 작업을 완료하세요.
- Ed25519 키 쌍을 생성하고 공개 키를 JSON 웹 키 (JWK) 세트로 형식 지정합니다.
- 잘 알려진 HTTPS 엔드포인트에서 공개 키 디렉터리를 호스팅합니다.
- 비공개 키로 각 아웃바운드 HTTP 요청에 서명합니다.
- 선택사항: 실험용
Agent-Context헤더에 서명하여 서명 키를 공유하는 테넌트 간에 평판을 격리합니다.
Ed25519 키 쌍 생성
WBA에는 Ed25519 (RFC 8032) 비대칭 키 쌍이 필요합니다. 비공개 키를 비공개로 유지하여 아웃바운드 HTTP 요청에 서명하고 JSON 웹 키 (JWK) 세트 (RFC 7517)에 공개 키를 게시합니다.
OpenSSL 및 jwker를 사용하여 키 쌍 생성
OpenSSL을 사용하여 Ed25519 키 쌍을 생성하고 오픈소스 jwker 도구를 사용하여 공개 키를 JWK로 변환할 수 있습니다.
PEM 형식으로 Ed25519 비공개 키를 생성합니다.
openssl genpkey -algorithm ed25519 -out private-key.pemPEM 형식으로 공개 키를 추출합니다.
openssl pkey -in private-key.pem -pubout -out public-key.pemjwker가 아직 설치되어 있지 않으면jwker설치 안내에 따라 설치합니다.jwker를 사용하여 PEM 공개 키를 JWK 파일로 변환합니다.jwker public-key.pem public-key.jwk이 명령어는 단일 JWK 객체를
public-key.jwk에 작성합니다.{ "kty": "OKP", "crv": "Ed25519", "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs" }JWK 객체를 JWK 집합
keys배열 (jwks.json) 내에 래핑하고 필수alg,use,kid필드를 추가합니다.{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "use": "sig", "kid": "OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw", "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs" } ] }
공개 JWK 세트 필드 참조
JWK 세트 keys 배열의 각 공개 키 객체는 다음 필드를 지원합니다.
| 필드 | 값 | 설명 |
|---|---|---|
kty |
"OKP" |
키 유형입니다. RFC 8037에 정의된 "OKP" (옥텟 키 쌍)이어야 합니다. |
crv |
"Ed25519" |
암호화 곡선입니다. "Ed25519"이어야 합니다. |
alg |
"EdDSA" |
키와 함께 사용하려는 알고리즘입니다. "EdDSA"이어야 합니다. |
use |
"sig" |
공개 키의 의도된 용도입니다. "sig" (서명)로 설정합니다. |
x |
"BASE64URL_PUBLIC_KEY" |
패딩되지 않은 base64url (RFC 4648 섹션 5)을 사용하여 인코딩된 32바이트 원시 Ed25519 공개 키입니다. |
kid |
"KEY_ID" |
서명된 요청의 keyid 매개변수와 일치하는 고유 키 식별자입니다. 교차 공급업체 호환성을 위해 RFC 7638 SHA-256 JWK 지문 (패딩되지 않은 base64url)을 사용합니다. |
nbf |
UNIX_SECONDS |
(선택사항) 키가 유효하지 않은 상태가 되기 전의 Unix 타임스탬프(초)입니다. |
exp |
UNIX_SECONDS |
(선택사항) 키가 만료되는 Unix 타임스탬프(초)입니다. 검증자는 만료된 키를 거부합니다. |
공개 키 디렉터리 호스팅
확인자는 에이전트 도메인의 잘 알려진 HTTPS 엔드포인트에서 공개 키를 가져옵니다.
엔드포인트 및 헤더 요구사항
키 디렉터리를 호스팅하려면 다음 요구사항을 충족하도록 웹 서버를 구성합니다.
- URL 경로:
https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory에서 JWK 세트 JSON을 제공합니다. - 전송 보안: 기본 포트
443에서 HTTPS를 통해 엔드포인트를 제공합니다. 요청을 다른 호스트나 스키마로 리디렉션하지 마세요. - 콘텐츠 유형:
Content-Type응답 헤더를application/http-message-signatures-directory+json(또는application/json)로 설정합니다. - 캐싱:
max-age지시문 (예:Cache-Control: public, max-age=3600)을 사용하여Cache-Control응답 헤더를 설정합니다. - 키 순환: 새 비공개 키로 요청에 서명하기 전에 공개 JWK를
keys배열에 추가하고Cache-ControlTTL이 만료될 때까지 기다립니다.keys배열에서 취소되거나 만료된 키를 삭제합니다.
다음 예시는 GET /.well-known/http-message-signatures-directory의 HTTP 응답을 보여줍니다.
HTTP/1.1 200 OK
Content-Type: application/http-message-signatures-directory+json
Cache-Control: public, max-age=3600
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"use": "sig",
"kid": "OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw",
"x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
}
]
}
아웃바운드 HTTP 요청 서명
WBA를 사용하여 아웃바운드 HTTPS 요청을 인증하려면 Signature-Agent, Signature-Input, Signature라는 세 가지 필수 헤더를 연결합니다.
HTTP 헤더
Signature-Agent: 키 디렉터리의 HTTPS 출처를 따옴표로 묶은 문자열로 지정합니다.Signature-Agent: "https://agent.example.com"Signature-Input: 서명 라벨 (예:sig1)을 포함된 HTTP 구성요소 및 서명 메타데이터 매개변수의 내부 목록에 매핑합니다.Signature-Input: sig1=("@authority" "signature-agent");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"다음과 같은 대상 구성요소 및 매개변수를 포함합니다.
- 적용되는 구성요소:
"@authority": (필수사항) 서명을 대상 호스트에 바인딩합니다 (예:www.example.com)."signature-agent": (필수사항) 서명을Signature-Agent헤더에 바인딩합니다.
- 메타데이터 매개변수:
created: 현재 Unix 타임스탬프 (초 단위, 정수)입니다.expires: 만료 Unix 타임스탬프 (초 단위, 정수)입니다.created후expires를 최대 300초 (5분)로 설정합니다.keyid: JWK 세트의 공개 키kid문자열입니다.tag:"web-bot-auth"이어야 합니다.
- 적용되는 구성요소:
Signature: 동일한 서명 라벨 (sig1)을 RFC 8941 바이트 시퀀스(:콜론으로 래핑됨)로 형식이 지정된 base64로 인코딩된 Ed25519 서명에 매핑합니다.Signature: sig1=:BASE64_ENCODED_SIGNATURE:
서명 기반 구성
서명하기 전에 RFC 9421 섹션 2.5에 정의된 표준 서명 기본 문자열을 구성합니다. 각 대상 구성요소는 소문자로 한 줄에 하나씩 표시되고 그 뒤에 줄바꿈 없이 @signature-params 줄이 표시됩니다.
"@authority": www.example.com
"signature-agent": "https://agent.example.com"
"@signature-params": ("@authority" "signature-agent");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
Ed25519 비공개 키를 사용하여 이 서명 기본 문자열의 UTF-8 바이트를 서명합니다.
Go 예시
다음 예시에서는 OpenSSL에서 생성된 private-key.pem 파일을 로드하고 Go의 표준 crypto/ed25519 패키지와 github.com/dunglas/httpsfv를 사용하여 발신 *http.Request에 서명합니다.
package main
import (
"crypto/ed25519"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
"strings"
"time"
"github.com/dunglas/httpsfv"
)
// loadPrivateKey parses a PKCS#8 PEM-encoded Ed25519 private key
// generated by OpenSSL.
func loadPrivateKey(pemBytes []byte) (ed25519.PrivateKey, error) {
// Decode the PEM armor to get the raw DER bytes.
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
// OpenSSL generates private keys in PKCS#8 format by default.
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
// Confirm that the parsed key is an Ed25519 private key.
privKey, ok := key.(ed25519.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an Ed25519 private key")
}
return privKey, nil
}
// signRequestEd25519 signs req with WBA headers.
func signRequestEd25519(
req *http.Request,
privKey ed25519.PrivateKey,
keyID string,
) error {
const (
// sigLabel links the Signature-Input and Signature entries.
sigLabel = "sig1"
// agentURL is the HTTPS origin where your JWK Set is hosted.
agentURL = "https://agent.example.com"
)
// 1. Set the Signature-Agent header as a quoted string.
sigAgentHeader := fmt.Sprintf("%q", agentURL)
req.Header.Set("Signature-Agent", sigAgentHeader)
// 2. List the HTTP components to cover in the signature.
// Always cover "@authority" (the target host) and "signature-agent".
authority := strings.ToLower(req.URL.Host)
items := []httpsfv.Item{
httpsfv.NewItem("@authority"),
httpsfv.NewItem("signature-agent"),
}
baseLines := []string{
fmt.Sprintf(`"@authority": %s`, authority),
fmt.Sprintf(`"signature-agent": %s`, sigAgentHeader),
}
// If the optional Agent-Context header is set, sign it too.
if agentCtx := req.Header.Get("Agent-Context"); agentCtx != "" {
items = append(items, httpsfv.NewItem("agent-context"))
line := fmt.Sprintf(`"agent-context": %s`, agentCtx)
baseLines = append(baseLines, line)
}
// 3. Set the signature metadata parameters.
// Set expires to 300 seconds (5 minutes) after created.
now := time.Now().Unix()
params := httpsfv.NewParams()
params.Add("created", now)
params.Add("expires", now+300)
params.Add("keyid", keyID)
params.Add("tag", "web-bot-auth")
// Serialize the covered components and parameters for Signature-Input.
innerList := httpsfv.InnerList{
Items: items,
Params: params,
}
sigParamsStr, err := httpsfv.Marshal(innerList)
if err != nil {
return err
}
// 4. Build the RFC 9421 signature base string.
// Join each covered component line and @signature-params with "\n".
// Don't add a trailing newline after the @signature-params line.
paramLine := fmt.Sprintf(`"@signature-params": %s`, sigParamsStr)
baseLines = append(baseLines, paramLine)
sigBase := strings.Join(baseLines, "\n")
// 5. Sign the UTF-8 bytes of the signature base with your private key.
sigBytes := ed25519.Sign(privKey, []byte(sigBase))
sigB64 := base64.StdEncoding.EncodeToString(sigBytes)
// 6. Attach the Signature-Input and Signature headers to the request.
req.Header.Set("Signature-Input", fmt.Sprintf("%s=%s", sigLabel, sigParamsStr))
req.Header.Set("Signature", fmt.Sprintf("%s=:%s:", sigLabel, sigB64))
return nil
}
실험용 Agent-Context 헤더 포함
멀티 테넌트 플랫폼 (예: 호스팅된 브라우저 제공업체 또는 클라우드 에이전트 런타임)은 단일 공유 서명 키를 사용하여 여러 고객 에이전트 또는 테넌트의 아웃바운드 요청에 서명하는 경우가 많습니다. 기본적으로 WBA는 서명 도메인 (Signature-Agent)만 식별하므로 해당 키로 서명된 모든 트래픽은 동일한 평판을 공유합니다.
Agent-Context 헤더를 포함하고 서명하면 reCAPTCHA는 플랫폼에서 공통 서명 키를 공유하는 여러 제품, 프로젝트, 사용자 또는 세션에서 평판을 개별적으로 평가할 수 있습니다.
Agent-Context 헤더 형식 지정
Agent-Context를 RFC 8941 구조화된 필드 사전으로 지정합니다. 여기서 각 키는 소문자 토큰이고 각 값은 따옴표로 묶인 ASCII 문자열입니다.
Agent-Context: product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"
모든 입력란은 선택사항입니다. 아키텍처에 적용되는 필드만 포함합니다.
| 필드 | 설명 | 예 |
|---|---|---|
product |
요청을 보내는 에이전트의 애플리케이션 또는 상업용 이름입니다. | product="ResearchBot" |
project |
호스트 플랫폼의 가명처리된 계정, 작업공간 또는 테넌트 프로젝트 식별자입니다. | project="prj_89a0f" |
user |
고객 또는 최종 사용자의 가명 식별자 (예: 불투명 UUID 또는 솔트 해시)입니다. 평판을 쌓기 위해 동일한 사용자의 요청에서 이 값을 안정적으로 유지하고 실제 개인 식별 정보를 포함하지 마세요. | user="usr_29df3" |
session |
단일 작업, 워크플로 실행 또는 탐색 세션의 임시 불투명 식별자입니다. | session="sess_88b1" |
Agent-Context 헤더 서명
reCAPTCHA는 WBA 서명에 "agent-context"를 포함하는 경우에만 평판 격리에 Agent-Context를 사용합니다. 서명되지 않은 Agent-Context 헤더는 무시됩니다.
Agent-Context 헤더에 서명하려면 다음 단계를 완료하세요.
signRequestEd25519를 호출하기 전에 발신*http.Request에서Agent-Context헤더를 설정합니다.req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)Signature-Input구성요소 목록 및 서명 기본 문자열에"agent-context"를 포함합니다(Go 예시에서 자동으로 처리됨)."@authority": www.example.com "signature-agent": "https://agent.example.com" "agent-context": product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1" "@signature-params": ("@authority" "signature-agent" "agent-context");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
다음 예시에서는 Agent-Context로 서명된 전체 HTTP 요청을 보여줍니다.
GET /api/data HTTP/1.1
Host: www.example.com
Signature-Agent: "https://agent.example.com"
Agent-Context: product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"
Signature-Input: sig1=("@authority" "signature-agent" "agent-context");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
Signature: sig1=:BASE64_ENCODED_SIGNATURE:
다음 단계
WBA로 요청에 서명하면 에이전트 도메인의 암호화 제어가 증명되지만, 웹사이트에는 에이전트를 운영하는 주체와 에이전트의 목적에 관한 메타데이터도 필요합니다.
- 사이트 소유자가 인증된 에이전트 신호를 사용하는 방법 알아보기:
- 에이전트 개요 대시보드에서 에이전트 트래픽을 모니터링합니다.
verified_bots조건 변수를 사용하여 챌린지 정책을 구성합니다.
- 프로토콜 사양 읽기: