mcpField
C
consent-receipt-spec

consent-receipt-spec

github.com/mukul975セキュリティ32336 回インストール32 週間前に更新
リポジトリ

consent-receipt-spec について

同意レシートを扱うプライバシーエンジニアやコンプライアンス実務者向けのスキルです。Kantara v1.1 の機械可読形式と ISO/IEC 27560:2023 との対応に加え、JWT 検証とライフサイクル管理を説明します。

主な機能

  1. 01Kantara レシートの主要フィールドを説明
  2. 02管理者・サービス・目的の構造を定義
  3. 03機械可読な JSON レシート形式を提示
  4. 04JWT による署名検証を網羅
  5. 05レシートの更新・撤回・期限切れを管理

活用シーン

  1. 01標準化された同意記録の実装
  2. 02個人が追跡できる機械可読レシートの生成
  3. 03発行済みレシートの真正性の検証
  4. 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.md
references/
standards.md
workflows.md
scripts/
process.py

詳細

Kantara Initiative 同意レシート仕様

概要

同意レシートとは、同意が与えられた証拠として、個人(データ主体)に提供される同意取引の記録です。Kantara Initiative Consent Receipt Specification v1.1 は、個人が複数の組織にわたって同意を追跡および管理できるようにする、標準的な機械可読形式を定義しています。このスキルでは、Kantara 仕様および ISO/IEC 27560:2023 に準拠した同意レシートの実装を扱います。

同意レシートの構造

コアフィールド(Kantara v1.1)
フィールド型必須説明
versionStringはい仕様バージョン(例:"KI-CR-v1.1.0")
jurisdictionStringはい法的管轄区域(ISO 3166-1 alpha-2)
consentTimestampDateTimeはい同意付与時の UTC タイムスタンプ
collectionMethodStringはい同意の収集方法(ウェブフォーム、口頭、書面)
consentReceiptIDUUIDはいこのレシートの一意の識別子
publicKeyStringいいえレシート検証用の公開鍵
languageStringはい同意取得時に使用した言語(BCP 47)
piiPrincipalIdStringはいデータ主体の識別子
piiControllersArrayはいデータ管理者の一覧
policyUrlURLはいプライバシーポリシーへのリンク
servicesArrayはい同意の対象となるサービス
sensitiveBooleanはい特別カテゴリデータが含まれるかどうか
spiCatArrayいいえ処理される特別カテゴリのデータ
PII 管理者オブジェクト
フィールド型必須説明
piiControllerStringはい管理者組織の名称
onBehalfBooleanいいえ別の管理者を代理して行動しているかどうか
contactStringはい管理者の連絡先情報
addressObjectはい管理者の所在地
emailStringはい連絡先メールアドレス
phoneStringいいえ連絡先電話番号
piiControllerUrlURLいいえ管理者のウェブサイトの URL
サービスオブジェクト
フィールド型必須説明
serviceStringはいサービス名
purposesArrayはい処理目的の一覧
目的オブジェクト
フィールド型必須説明
purposeStringはい処理目的の説明
purposeCategoryArrayはい目的のカテゴリコード
consentTypeStringはい"explicit" または "implicit"
piiCategoryArrayはい処理される PII のカテゴリ
primaryPurposeBooleanはいこれが主要な目的かどうか
terminationStringはい同意の撤回方法
thirdPartyDisclosureBooleanはいデータが第三者と共有されるかどうか
thirdPartyNameStringいいえ第三者の名称(該当する場合)

機械可読レシート形式

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同意の日時同意が取得された日時
piiPrincipalIdPII 主体識別子データ主体の識別子
piiControllersPII 管理者データを処理する組織
services.purposes目的処理の目的
services.purposes.piiCategoryPII カテゴリー個人データの種類
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 — 機械可読な個人プライバシー条件に関する規格

コメント