디바이스 바인딩 (PoP)
핵심 요약
기기 바인딩은 기기 키와 요청별 증명을 확인해 사용권 토큰만 복사한 요청을 차단합니다.
이미 장악된 브라우저에서 기능을 사용하는 행위까지 막지는 못합니다. 호환 클라이언트 제작 시에만 상세 서명 규격을 확인하세요.
디바이스 바인딩은 entitlement를 특정 디바이스 키페어에 묶어 복사된 토큰만으로는 무용하게 합니다: 게이트된 모든 호출이 license_bindings에 바인딩된 공개 절반에 대응하는 개인 키의 소유를 다시 증명합니다.
jkt — 바인딩 식별자
jkt = sha256(sha256(RFC 7638 JWK thumbprint)) — DevPay 고유 이중 해시(experiments/secure-browser-licensing/tools/sdk-worker-contract.test.mjs에서 검증됨)로, RFC 9449 thumbprint가 아닙니다. Entitlement는 이를 두 번 가집니다: jkt_bound + cnf.jkt(동일 값; 둘 다 있으면 SDK가 교차 검사).
디바이스 키페어
- 브라우저(
@yetdapay/license-web): WebCrypto P-256이extractable:false로 생성되고 IndexedDB에CryptoKey로 영속됨 (createIndexedDbKeyStore). JS는 개인 키를 export할 수 없습니다 — 하지만 내부 남용(적대적 코드가sign()호출)과 전체 프로필 복제는 여전히 가능합니다; 위협 모델 참조. - 슬롯: 활성화가
(lic_id, jkt)를license_bindings에 삽입합니다 (UNIQUE제약);countBindings < max_devices또는403 DEVICE_LIMIT. 동일jkt의 재활성화는 멱등. max_devices는 라이선스별입니다(seller_settings.default_max_devices가 새 라이선스를 시딩). 현재 언바인드 경로가 없습니다 —resetDevice()는 로컬 아이덴티티를 지우지만 슬롯을 영구 소진합니다 (기록된 갭).
증명 — DPoP: <proof-jws>
디바이스 키로 서명된 요청당 JWS, 페이로드:
{
"htu": "/api/v1/licenses/execute?x=1", // 동일 origin pathname + query
"htm": "POST",
"iat": 1730000000,
"jti": "<nonce>",
"h": "base64url(sha256(raw request body))",
"jwk": { /* public jwk */ }
}2
3
4
5
6
7
8
서버 검사(가드 순서): 증명 서명 → htm이 메서드와 일치 → htu가 요청 대상과 일치 → iat가 ±60초 내 → jti가 미사용(license_nonces에 원자적 소비, TTL 600초) → jwk의 jkt가 바인딩된 jkt_bound와 일치 → h가 sha256(body)와 일치.
실패 시 → 401 UNAUTHORIZED — 모든 검사에 하나의 불투명 코드 (의도적; 구체적 사유는 응답이 아닌 security_events에 기록).
바인딩이 주는 것
- 토큰 도난으로는 부족 — 도둑은 개인 키도 필요합니다.
- 리플레이는 경계됨 —
jti논스는 일회용; 오래된iat거부. - Body 변조 실패 —
h가 정확한 요청 바이트를 바인딩.
주지 않는 것
- 사람을 증명하지 않습니다 — 유료 사용자가 승인된 호출을 릴레이할 수 있습니다(미터링/레이트 리밋이 경계).
- 전체 프로필+키 복제에서는 살아남지 못합니다 — 그것이 곧 소유입니다.
- 셀러가 자기 BYOS 서버에서 검사를 제거하는 것을 막지 못합니다 (다른 신뢰 도메인).