consent-receipt-spec について
同意レシートを扱うプライバシーエンジニアやコンプライアンス実務者向けのスキルです。Kantara v1.1 の機械可読形式と ISO/IEC 27560:2023 との対応に加え、JWT 検証とライフサイクル管理を説明します。
主な機能
- 01Kantara レシートの主要フィールドを説明
- 02管理者・サービス・目的の構造を定義
- 03機械可読な JSON レシート形式を提示
- 04JWT による署名検証を網羅
- 05レシートの更新・撤回・期限切れを管理
活用シーン
- 01標準化された同意記録の実装
- 02個人が追跡できる機械可読レシートの生成
- 03発行済みレシートの真正性の検証
- 04レシートの状態と更新の管理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mukul975/ privacy-data-protection-skills --skill consent-receipt-specインストール後、Claude Code で/consent-receipt-specを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/template.mdreferences/standards.mdworkflows.mdscripts/process.py詳細
Kantara Initiative 同意レシート仕様
概要
同意レシートとは、同意が与えられた証拠として、個人(データ主体)に提供される同意取引の記録です。Kantara Initiative Consent Receipt Specification v1.1 は、個人が複数の組織にわたって同意を追跡および管理できるようにする、標準的な機械可読形式を定義しています。このスキルでは、Kantara 仕様および ISO/IEC 27560:2023 に準拠した同意レシートの実装を扱います。
同意レシートの構造
コアフィールド(Kantara v1.1)
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| version | String | はい | 仕様バージョン(例:"KI-CR-v1.1.0") |
| jurisdiction | String | はい | 法的管轄区域(ISO 3166-1 alpha-2) |
| consentTimestamp | DateTime | はい | 同意付与時の UTC タイムスタンプ |
| collectionMethod | String | はい | 同意の収集方法(ウェブフォーム、口頭、書面) |
| consentReceiptID | UUID | はい | このレシートの一意の識別子 |
| publicKey | String | いいえ | レシート検証用の公開鍵 |
| language | String | はい | 同意取得時に使用した言語(BCP 47) |
| piiPrincipalId | String | はい | データ主体の識別子 |
| piiControllers | Array | はい | データ管理者の一覧 |
| policyUrl | URL | はい | プライバシーポリシーへのリンク |
| services | Array | はい | 同意の対象となるサービス |
| sensitive | Boolean | はい | 特別カテゴリデータが含まれるかどうか |
| spiCat | Array | いいえ | 処理される特別カテゴリのデータ |
PII 管理者オブジェクト
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| piiController | String | はい | 管理者組織の名称 |
| onBehalf | Boolean | いいえ | 別の管理者を代理して行動しているかどうか |
| contact | String | はい | 管理者の連絡先情報 |
| address | Object | はい | 管理者の所在地 |
| String | はい | 連絡先メールアドレス | |
| phone | String | いいえ | 連絡先電話番号 |
| piiControllerUrl | URL | いいえ | 管理者のウェブサイトの URL |
サービスオブジェクト
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| service | String | はい | サービス名 |
| purposes | Array | はい | 処理目的の一覧 |
目的オブジェクト
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| purpose | String | はい | 処理目的の説明 |
| purposeCategory | Array | はい | 目的のカテゴリコード |
| consentType | String | はい | "explicit" または "implicit" |
| piiCategory | Array | はい | 処理される PII のカテゴリ |
| primaryPurpose | Boolean | はい | これが主要な目的かどうか |
| termination | String | はい | 同意の撤回方法 |
| thirdPartyDisclosure | Boolean | はい | データが第三者と共有されるかどうか |
| thirdPartyName | String | いいえ | 第三者の名称(該当する場合) |
機械可読レシート形式
JSON 同意レシート
{
"version": "KI-CR-v1.1.0",
"jurisdiction": "EU",
"consentTimestamp": "2026-03-14T10:30:00.000Z",
"collectionMethod": "web_form",
"consentReceiptID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"language": "en",
"piiPrincipalId": "user-98765",
"piiControllers": [
{
"piiController": "Cipher Engineering Labs",
"onBehalf": false,
"contact": "Data Protection Officer",
"address": {
"streetAddress": "100 Technology Drive",
"locality": "London",
"region": "Greater London",
"postalCode": "EC2A 1NT",
"country": "GB"
},
"email": "[email protected]",
"phone": "+44-20-7946-0958",
"piiControllerUrl": "https://www.cipherengineeringlabs.com"
}
],
"policyUrl": "https://www.cipherengineeringlabs.com/privacy-policy",
"services": [
{
"service": "Privacy Analytics Platform",
"purposes": [
{
"purpose": "Provide personalized privacy compliance recommendations",
"purposeCategory": ["core_service"],
"consentType": "explicit",
"piiCategory": ["contact_information", "professional_information"],
"primaryPurpose": true,
"termination": "Account settings > Privacy > Withdraw consent",
"thirdPartyDisclosure": false
},
{
"purpose": "Send product updates and feature announcements",
"purposeCategory": ["marketing"],
"consentType": "explicit",
"piiCategory": ["contact_information"],
"primaryPurpose": false,
"termination": "Unsubscribe link in email or Account settings",
"thirdPartyDisclosure": false
},
{
"purpose": "Aggregate usage analytics to improve platform features",
"purposeCategory": ["analytics"],
"consentType": "explicit",
"piiCategory": ["usage_data", "device_information"],
"primaryPurpose": false,
"termination": "Account settings > Privacy > Analytics opt-out",
"thirdPartyDisclosure": true,
"thirdPartyName": "Cipher Analytics Processing Ltd"
}
]
}
],
"sensitive": false,
"spiCat": []
}
JWT ベースの検証メカニズム
署名付き同意レシートの実装
"""
Consent receipt generation and verification using JWT (JSON Web Tokens).
Implements Kantara Initiative Consent Receipt Specification v1.1
with cryptographic verification.
"""
import json
import uuid
from datetime import datetime, timezone
from typing import Optional
import jwt # PyJWT
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
class ConsentReceiptIssuer:
"""
Issue and sign consent receipts as JWTs.
Uses RS256 (RSA with SHA-256) for signing.
"""
def __init__(self, private_key_pem: bytes, issuer_name: str):
"""
Args:
private_key_pem: PEM-encoded RSA private key
issuer_name: Name of the issuing organization
"""
self.private_key = serialization.load_pem_private_key(
private_key_pem, password=None, backend=default_backend()
)
self.public_key = self.private_key.public_key()
self.issuer_name = issuer_name
def issue_receipt(
self,
principal_id: str,
services: list[dict],
jurisdiction: str,
collection_method: str,
policy_url: str,
sensitive: bool = False,
language: str = "en",
controller_info: dict = None
) -> tuple[str, str]:
"""
Generate a signed consent receipt.
Args:
principal_id: Data subject identifier
services: List of service/purpose objects
jurisdiction: Legal jurisdiction code
collection_method: How consent was collected
policy_url: URL of the privacy policy
sensitive: Whether special categories are involved
language: Language of consent interaction
controller_info: PII controller details
Returns:
Tuple of (receipt_id, signed_jwt_string)
"""
receipt_id = str(uuid.uuid4())
if controller_info is None:
controller_info = {
"piiController": self.issuer_name,
"onBehalf": False,
"contact": "Data Protection Officer",
"email": f"dpo@{self.issuer_name.lower().replace(' ', '')}.com"
}
receipt_payload = {
"version": "KI-CR-v1.1.0",
"jurisdiction": jurisdiction,
"consentTimestamp": datetime.now(timezone.utc).isoformat(),
"collectionMethod": collection_method,
"consentReceiptID": receipt_id,
"language": language,
"piiPrincipalId": principal_id,
"piiControllers": [controller_info],
"policyUrl": policy_url,
"services": services,
"sensitive": sensitive,
"spiCat": [],
# JWT standard claims
"iss": self.issuer_name,
"sub": principal_id,
"iat": int(datetime.now(timezone.utc).timestamp()),
"jti": receipt_id,
}
# Sign the receipt as a JWT
signed_token = jwt.encode(
receipt_payload,
self.private_key,
algorithm="RS256",
headers={"kid": f"{self.issuer_name}-consent-key-1"}
)
return receipt_id, signed_token
def get_public_key_pem(self) -> str:
"""Export the public key for verification by data subjects."""
return self.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode("utf-8")
class ConsentReceiptVerifier:
"""
Verify signed consent receipts.
Used by data subjects or auditors to confirm receipt authenticity.
"""
def __init__(self):
self.trusted_keys: dict[str, bytes] = {}
def register_issuer_key(self, issuer_name: str, public_key_pem: str):
"""Register a trusted issuer's public key."""
self.trusted_keys[issuer_name] = public_key_pem.encode("utf-8")
def verify_receipt(self, token: str) -> tuple[bool, Optional[dict], Optional[str]]:
"""
Verify a signed consent receipt JWT.
Returns:
Tuple of (is_valid, decoded_payload_or_None, error_message_or_None)
"""
try:
# Decode without verification first to get the issuer
unverified = jwt.decode(token, options={"verify_signature": False})
issuer = unverified.get("iss")
if issuer not in self.trusted_keys:
return (False, None, f"Unknown issuer: {issuer}")
public_key = serialization.load_pem_public_key(
self.trusted_keys[issuer],
backend=default_backend()
)
# Verify the signature
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
issuer=issuer
)
# Validate required Kantara fields
required_fields = [
"version", "jurisdiction", "consentTimestamp",
"collectionMethod", "consentReceiptID", "piiPrincipalId",
"piiControllers", "policyUrl", "services", "sensitive"
]
missing = [f for f in required_fields if f not in decoded]
if missing:
return (False, decoded, f"Missing required fields: {missing}")
return (True, decoded, None)
except jwt.ExpiredSignatureError:
return (False, None, "Receipt JWT has expired")
except jwt.InvalidSignatureError:
return (False, None, "Invalid signature — receipt may have been tampered with")
except jwt.DecodeError as e:
return (False, None, f"Failed to decode JWT: {str(e)}")
レシートのライフサイクル管理
ライフサイクルの状態
ISSUED --> ACTIVE --> WITHDRAWN
| | |
| v v
| UPDATED ARCHIVED
| |
v v
EXPIRED ACTIVE (new version)
ライフサイクルマネージャー
"""
Manage the lifecycle of consent receipts including
updates, withdrawals, and expiration tracking.
"""
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
class ReceiptStatus(Enum):
ISSUED = "issued"
ACTIVE = "active"
UPDATED = "updated"
WITHDRAWN = "withdrawn"
EXPIRED = "expired"
ARCHIVED = "archived"
@dataclass
class ReceiptRecord:
receipt_id: str
principal_id: str
status: ReceiptStatus
issued_at: datetime
last_updated: datetime
withdrawn_at: datetime | None
jwt_token: str
version: int
superseded_by: str | None
purposes: list[str]
class ConsentReceiptLifecycle:
"""
Manage consent receipt state transitions and history.
"""
def __init__(self, receipt_store, issuer: ConsentReceiptIssuer):
self.store = receipt_store
self.issuer = issuer
def activate_receipt(self, receipt_id: str) -> bool:
"""Transition a receipt from ISSUED to ACTIVE."""
record = self.store.get(receipt_id)
if record and record.status == ReceiptStatus.ISSUED:
record.status = ReceiptStatus.ACTIVE
record.last_updated = datetime.now(timezone.utc)
self.store.update(record)
return True
return False
def update_receipt(
self,
receipt_id: str,
updated_services: list[dict],
jurisdiction: str,
policy_url: str
) -> str | None:
"""
Update an existing receipt by issuing a new version.
The old receipt is marked as UPDATED and linked to the new one.
Returns the new receipt ID or None if update failed.
"""
old_record = self.store.get(receipt_id)
if not old_record or old_record.status not in [
ReceiptStatus.ACTIVE, ReceiptStatus.ISSUED
]:
return None
# Issue new receipt
new_receipt_id, new_jwt = self.issuer.issue_receipt(
principal_id=old_record.principal_id,
services=updated_services,
jurisdiction=jurisdiction,
collection_method="consent_update",
policy_url=policy_url
)
# Create new record
new_record = ReceiptRecord(
receipt_id=new_receipt_id,
principal_id=old_record.principal_id,
status=ReceiptStatus.ACTIVE,
issued_at=datetime.now(timezone.utc),
last_updated=datetime.now(timezone.utc),
withdrawn_at=None,
jwt_token=new_jwt,
version=old_record.version + 1,
superseded_by=None,
purposes=[p["purpose"] for s in updated_services for p in s.get("purposes", [])]
)
self.store.save(new_record)
# Mark old receipt as updated
old_record.status = ReceiptStatus.UPDATED
old_record.superseded_by = new_receipt_id
old_record.last_updated = datetime.now(timezone.utc)
self.store.update(old_record)
return new_receipt_id
def withdraw_consent(self, receipt_id: str, reason: str = "") -> bool:
"""
Withdraw consent and mark the receipt accordingly.
Returns True if withdrawal was successful.
"""
record = self.store.get(receipt_id)
if not record or record.status not in [
ReceiptStatus.ACTIVE, ReceiptStatus.ISSUED
]:
return False
record.status = ReceiptStatus.WITHDRAWN
record.withdrawn_at = datetime.now(timezone.utc)
record.last_updated = datetime.now(timezone.utc)
self.store.update(record)
return True
def get_active_receipts(self, principal_id: str) -> list[ReceiptRecord]:
"""Get all active consent receipts for a data subject."""
return self.store.find_by_principal(
principal_id, status=ReceiptStatus.ACTIVE
)
def get_receipt_history(self, principal_id: str) -> list[ReceiptRecord]:
"""Get the complete consent receipt history for a data subject."""
return self.store.find_by_principal(principal_id)
ISO/IEC 27560:2023 との整合性
| Kantara CR フィールド | ISO 27560 相当項目 | 備考 |
|---|---|---|
| consentReceiptID | 同意記録識別子 | 記録の一意の識別子 |
| consentTimestamp | 同意の日時 | 同意が取得された日時 |
| piiPrincipalId | PII 主体識別子 | データ主体の識別子 |
| piiControllers | PII 管理者 | データを処理する組織 |
| services.purposes | 目的 | 処理の目的 |
| services.purposes.piiCategory | PII カテゴリー | 個人データの種類 |
| sensitive | 機微な PII の指標 | 特別カテゴリーのフラグ |
| jurisdiction | 管轄区域 | 適用される法的枠組み |
| collectionMethod | 同意の取得方法 | 同意を取得した方法 |
| policyUrl | プライバシー通知の参照先 | プライバシー通知へのリンク |
参考資料
- Kantara Initiative Consent Receipt Specification v1.1.0(2018)
- ISO/IEC 27560:2023 — プライバシー技術 — 同意記録の情報構造
- ISO/IEC 29184:2020 — オンラインプライバシー通知および同意に関するガイドライン
- RFC 7519 — JSON Web Token(JWT)
- W3C Data Privacy Vocabularies and Controls Community Group
- IEEE P7012 — 機械可読な個人プライバシー条件に関する規格
コメント
Markdown 対応