イタリアのデジタル アイデンティティの風景

イタリアのデジタル ID エコシステムは、ヨーロッパで最も複雑なエコシステムの 1 つであり、次の 3 つがあります。 共存および統合される主要システム: SPID (パブリックアイデンティティシステム デジタル)、 CIE (電子 ID カード)、および支払いの場合、 PayPA。 2023年からは、 OpenID コネクト プロトコルとして 統合により、開発者にとって統合が大幅に簡素化されました。

政府認証をアプリケーションに統合する必要がある開発者またはアーキテクトは、次のような問題に直面します。 重大な影響をもたらすいくつかの技術的な選択。この記事では、AGID のオンボーディングについて説明します SAML 2.0 (歴史的な SPID プロトコル) と OpenID Connect を比較しながら、具体的な実装まで説明します。 フェデレーション (SPID と CIE 両方の将来)、および支払いのために pagoPA を統合する方法を示します。

何を学ぶか

  • SPID アーキテクチャ: ID プロバイダー、サービス プロバイダー、SAML 2.0、およびセキュリティ レベル
  • CIE アーキテクチャ: NFC チップ、PIN、CIE-ID バックエンドおよび認証モード
  • SPID および CIE 用の OpenID Connect: フェデレーション、トークン、クレーム、およびスコープ
  • AGID オンボーディング プロセス: 技術的および手順上の要件
  • 公式SDK: 5つのプログラミング言語のライブラリ
  • 実際の実装: Python と TypeScript と完全な例
  • pagoPA: PA サービスへの支払いの統合
  • 開発と検証のためのテスト環境とステージング環境

SPID: パブリックデジタルアイデンティティシステム

SPID は、イタリア国家デジタル ID システムであり、 法令 82/2005 (CAD) AgID によって規制されています。これにより、国民はあらゆる PA サービス (および多くの PA サービス) で認証できるようになります。 private) の 1 組の資格情報を使用し、次のいずれかによって管理されます。 アイデンティティプロバイダー (IdP) 認定済み (Aruba、Infocert、Namirial、Poste、Register、Sielte、SpidItalia、Tim、Intesa)。

SPID 予測 3レベルのセキュリティ:

  • レベル1: ユーザー名とパスワードによる認証。リスクの低いサービスに適しています。
  • レベル2: ユーザー名/パスワード + OTP (SMS またはアプリ)。 PA サービスで最もよく使用されるレベル。 認証の第 2 要素 (2FA) が必要です。
  • レベル3: デジタル証明書またはスマート カードによる認証。高性能なサービスを目指して リスク(公正証書、適格なデジタル署名)。

CIE: 電子 ID カード

米国立印刷造幣局が発行した CIE 3.0 には、証明書付きの NFC チップが含まれています 強力な認証を可能にする X.509 デジタル デバイス。 CIE-IDシステムによりオンライン認証が可能 経由:

  • NFC搭載スマートフォン: ユーザーは CIE をスマートフォンに近づけて PIN を入力します。 CIE ID アプリは、デジタル署名された認証アサーションを生成します。
  • NFCリーダーを備えたデスクトップ: 内務省の CIE ID ソフトウェア経由。
  • NFC非搭載のデスクトップ: CIE ID アプリでスキャンした QR コードによる認証。

開発者の観点から見ると、CIE と SPID は同じ OpenID Connect (OIDC) プロトコルを共有するようになりました。 相違点は主にアイデンティティ プロバイダーの登録とメタデータです。

SPID および CIE 用の OpenID Connect: 統一プロトコル

AgID が公開した SPID および CIE に関する OpenID Connect 技術ルール、 docs.italia.it で入手できます。これらのルールは、 OpenID Connect フェデレーション OIDC Federation 1.0 標準 (IETF ドラフト) に基づいており、以下のとおりです。

  • AgID そこにあります トラストアンカー: フェデレーションのルート ノード、 すべての参加者間の信頼の信頼の源。
  • アイデンティティプロバイダー (SPIDと同じ) リーフノード 登録する AgIDで。
  • I 依拠当事者 (SPID/CIEを使用したいサービス)公開による登録 ある エンティティの構成 (公開鍵で署名された JWT)。
# Implementazione OpenID Connect per SPID/CIE in Python
# Usa la libreria spid-cie-oidc-django o implementazione custom

import httpx
import jwt
import json
import secrets
import hashlib
import base64
from datetime import datetime, timedelta
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

class SPIDCIEOIDCClient:
    """
    Client OIDC per integrazione SPID/CIE.
    Implementa il flow Authorization Code con PKCE (obbligatorio per SPID/CIE OIDC).
    """

    def __init__(
        self,
        client_id: str,       # URI del Relying Party (il tuo servizio)
        redirect_uri: str,    # URI di callback
        private_key_path: str, # Chiave privata RSA/EC per firma JWT
        trust_anchor: str = "https://registry.agid.gov.it"
    ):
        self.client_id = client_id
        self.redirect_uri = redirect_uri
        self.trust_anchor = trust_anchor

        # Carica chiave privata per firma
        with open(private_key_path, "rb") as f:
            self.private_key = serialization.load_pem_private_key(
                f.read(), password=None, backend=default_backend()
            )

    def generate_pkce(self) -> tuple[str, str]:
        """
        Genera code_verifier e code_challenge per PKCE.
        PKCE è OBBLIGATORIO nelle specifiche SPID/CIE OIDC.
        """
        # code_verifier: stringa casuale di 43-128 caratteri
        code_verifier = secrets.token_urlsafe(64)

        # code_challenge = BASE64URL(SHA256(code_verifier))
        code_challenge = base64.urlsafe_b64encode(
            hashlib.sha256(code_verifier.encode()).digest()
        ).rstrip(b"=").decode()

        return code_verifier, code_challenge

    def build_authorization_url(
        self,
        idp_authorization_endpoint: str,
        scope: list[str] = None,
        acr_values: str = "https://www.spid.gov.it/SpidL2",  # Livello 2 default
        state: str = None,
        nonce: str = None,
        ui_locales: str = "it",
        claims: dict = None
    ) -> tuple[str, dict]:
        """
        Costruisce l'URL di autorizzazione per SPID/CIE OIDC.
        Ritorna (authorization_url, session_data) dove session_data va salvato in sessione.
        """
        if scope is None:
            scope = ["openid", "profile"]

        if state is None:
            state = secrets.token_urlsafe(32)

        if nonce is None:
            nonce = secrets.token_urlsafe(32)

        code_verifier, code_challenge = self.generate_pkce()

        # Request Object: JWT firmato con claims della request
        # Obbligatorio in SPID/CIE OIDC per sicurezza end-to-end
        request_object_claims = {
            "iss": self.client_id,
            "aud": idp_authorization_endpoint,
            "iat": int(datetime.utcnow().timestamp()),
            "exp": int((datetime.utcnow() + timedelta(minutes=5)).timestamp()),
            "jti": secrets.token_urlsafe(16),
            "response_type": "code",
            "client_id": self.client_id,
            "redirect_uri": self.redirect_uri,
            "scope": " ".join(scope),
            "state": state,
            "nonce": nonce,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
            "acr_values": acr_values,
            "ui_locales": ui_locales,
        }

        if claims:
            request_object_claims["claims"] = claims

        # Firma il Request Object con la chiave privata del RP
        request_object = jwt.encode(
            request_object_claims,
            self.private_key,
            algorithm="RS256",
            headers={"kid": "rp-signing-key-2024"}
        )

        # Costruisci URL autorizzazione
        import urllib.parse
        params = {
            "client_id": self.client_id,
            "response_type": "code",
            "scope": " ".join(scope),
            "redirect_uri": self.redirect_uri,
            "state": state,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
            "request": request_object,  # Request Object firmato
        }

        auth_url = f"{idp_authorization_endpoint}?{urllib.parse.urlencode(params)}"

        session_data = {
            "state": state,
            "nonce": nonce,
            "code_verifier": code_verifier,
        }

        return auth_url, session_data

    async def exchange_code_for_tokens(
        self,
        authorization_code: str,
        code_verifier: str,
        idp_token_endpoint: str
    ) -> dict:
        """
        Scambia il codice di autorizzazione per i token (access_token, id_token).
        Usa Client Authentication con private_key_jwt (obbligatorio in SPID/CIE).
        """
        now = int(datetime.utcnow().timestamp())

        # client_assertion: JWT firmato per autenticare il RP al token endpoint
        client_assertion = jwt.encode(
            {
                "iss": self.client_id,
                "sub": self.client_id,
                "aud": idp_token_endpoint,
                "iat": now,
                "exp": now + 300,
                "jti": secrets.token_urlsafe(16),
            },
            self.private_key,
            algorithm="RS256",
            headers={"kid": "rp-signing-key-2024"}
        )

        async with httpx.AsyncClient() as client:
            response = await client.post(
                idp_token_endpoint,
                data={
                    "grant_type": "authorization_code",
                    "code": authorization_code,
                    "redirect_uri": self.redirect_uri,
                    "code_verifier": code_verifier,
                    "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                    "client_assertion": client_assertion,
                    "client_id": self.client_id,
                }
            )
            response.raise_for_status()
            return response.json()

    def validate_id_token(
        self,
        id_token: str,
        idp_jwks_uri: str,
        nonce: str,
        expected_acr: str = None
    ) -> dict:
        """
        Valida l'ID Token ricevuto dall'IdP.
        Verifica firma, nonce, audience, e livello di autenticazione (acr).
        """
        # Recupera le chiavi pubbliche dell'IdP
        import httpx
        jwks = httpx.get(idp_jwks_uri).json()

        # Decodifica e valida il JWT
        claims = jwt.decode(
            id_token,
            jwks,
            algorithms=["RS256", "ES256"],
            audience=self.client_id,
            options={"require": ["nonce", "acr"]}
        )

        # Verifica nonce (anti-replay)
        if claims.get("nonce") != nonce:
            raise ValueError("Invalid nonce in ID Token")

        # Verifica livello di autenticazione se richiesto
        if expected_acr and claims.get("acr") != expected_acr:
            raise ValueError(f"ACR mismatch: expected {expected_acr}, got {claims.get('acr')}")

        return claims

SPID および CIE クレーム: ユーザー属性

SPID と一般的なプライベート OIDC プロバイダーの重要な違いは、 主張 (属性 ユーザー) が利用可能です。 SPID/CIE は、イタリア政府によって認定された一連の属性を提供します。

OIDC を請求する SPID 属性 CIEで入手可能 注意事項
sub IdP での一意の ID Si それはCFではありません。異なる IdP 間の変更
fiscal_number 納税者IDコード Si 形式: TINIT-XXXXXXXXXXXXXX
given_name 名前 Si OIDC規格
family_name Cognome Si OIDC規格
birthdate 生年月日 Si 形式: YYYY-MM-DD
place_of_birth 出生地 Si 市区町村の地籍コード
gender セックス Si 男/女
email 電子メール (認証されていません) No ユーザーが宣言した SPID のみ
mobile_phone 携帯電話 No ユーザーが宣言した SPID のみ
document_details 文書データ Si CIE のみ: 数値。 CIE、有効期限、一般的な問題

警告: sub と Financial_number

Il sub SPID のクレームはユーザー識別子です 特定の IdP で、 グローバル識別子ではありません。ユーザーが IdP を変更した場合 (Aruba から Poste など)、 sub 変化。 税法 (fiscal_number) は国民の唯一の安定したグローバルな識別子です イタリア人。を使用します。 fiscal_number データベースの主キーとしてではなく、 sub.

AGID のオンボーディング: サービス プロバイダーになる

SPID または CIE OIDC をサービスに統合するには、次のとおり認定プロセスを完了する必要があります。 依拠当事者 (RP) AgIDで。プロセスは次のように分かれています。

  1. developers.italia.it への登録: アカウントを作成してプラットフォームにログインします SPID/CIE オンボーディングの。
  2. 技術的な準備: 公式 SDK または実装のいずれかを使用して SP を実装します。 カスタム。証明書利用者のメタデータ (エンティティ構成 JWT) を構成します。
  3. ステージング環境: AGID はテスト IdP (SAML の場合は spid-test.agid.gov.it、 Demo-oidc.agid.gov.it (OIDC 用) と事前定義されたテスト ユーザー。
  4. 技術的検証: SP は公式 AGID 検証ツールでテストされています。 すべての必須テスト ケースに合格する必要があります。
  5. 法的合意: AgID (または選択したアグリゲーターとのメンバーシップ契約の署名) アグリゲータ経由のメンバーシップの場合)。
  6. 生産: 承認後、SP は本番環境とユーザーに登録されます。 実在の人物を認証できます。
# Entity Configuration del Relying Party (JWT firmato)
# Questo documento deve essere pubblicato all'URL: {client_id}/.well-known/openid-federation

import jwt
import json
from datetime import datetime, timedelta

def generate_entity_configuration(
    client_id: str,  # URI del tuo servizio, es: https://servizi.miocomune.it
    private_key,
    public_key_jwk: dict,
    redirect_uris: list,
    organization_name: str,
    contacts: list
) -> str:
    """
    Genera l'Entity Configuration JWT per la registrazione OIDC Federation.
    Deve essere publicata a: {client_id}/.well-known/openid-federation
    """
    now = int(datetime.utcnow().timestamp())

    payload = {
        # Claims standard OIDC Federation
        "iss": client_id,
        "sub": client_id,
        "iat": now,
        "exp": now + 86400 * 365,  # Valida 1 anno (da aggiornare)
        "jwks": {"keys": [public_key_jwk]},  # Chiave pubblica del RP

        # Metadata del Relying Party
        "metadata": {
            "openid_relying_party": {
                "application_type": "web",
                "client_id": client_id,
                "client_registration_types": ["automatic"],
                "redirect_uris": redirect_uris,
                "response_types": ["code"],
                "grant_types": ["authorization_code"],
                "id_token_signed_response_alg": "RS256",
                "userinfo_signed_response_alg": "RS256",
                "token_endpoint_auth_method": "private_key_jwt",
                "token_endpoint_auth_signing_alg": "RS256",
                "scope": ["openid", "profile", "email", "offline_access"],
                "client_name": organization_name,
                "contacts": contacts,
                # Parametri obbligatori per SPID/CIE
                "policy_uri": f"{client_id}/privacy",
                "logo_uri": f"{client_id}/logo.png",
                "subject_type": "pairwise",
                "request_object_signing_alg": "RS256",
            }
        },

        # Trust chain verso la Trust Anchor di AgID
        "authority_hints": ["https://registry.agid.gov.it"],
    }

    return jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": "rp-signing-key-2024"})

# Endpoint FastAPI per esporre l'Entity Configuration
from fastapi import FastAPI
from fastapi.responses import Response

app = FastAPI()

@app.get("/.well-known/openid-federation")
async def entity_configuration():
    ec_jwt = generate_entity_configuration(
        client_id="https://servizi.miocomune.it",
        private_key=private_key,  # Caricata da HSM o file sicuro
        public_key_jwk=public_key_jwk,
        redirect_uris=["https://servizi.miocomune.it/auth/callback"],
        organization_name="Comune di Esempio",
        contacts=["tech@miocomune.it"]
    )

    return Response(
        content=ec_jwt,
        media_type="application/entity-statement+jwt"
    )

pagoPA: 支払いの統合

PayPA は、PagoPA S.p.A が管理する PA への支払いのための全国的なプラットフォームです。 これにより、国民は、広大なネットワークを通じて税金、罰金、料金、その他の PA サービスを支払うことができます。 チャネル (銀行、郵便局、支払いアプリ、Satispay など)。

技術的な観点から見ると、債権者機関 (EC) 向けの pagoPA 統合は以下を提供します。

  • pagoPA ノードのメンバーシップ: PagoPA メンバーシップ ポータル経由。参加できる組織 直接または認可された技術仲介者を通じて。
  • IUVの生成 (Unique Payment Identifier): 一意に識別するコード 予想される支払い。形式は債権者団体によって定義されますが、pagoPA 規則に準拠する必要があります。
  • 負債の状況: 国民が EC に対して負っているすべての負債はノードに記録されます 関連する IUV を伴う「負債ポジション」としての pagoPA。
  • 検証と閉鎖: pagoPA は支払いが完了すると EC に通知します。 債務ポジションを確認してクローズします。
# Integrazione pagoPA - Generazione posizione debitoria
# API SOAP/REST verso il Nodo pagoPA

import httpx
import uuid
from datetime import datetime, timedelta
from dataclasses import dataclass

@dataclass
class PaymentPosition:
    iuv: str               # Identificativo Univoco Versamento
    amount_cents: int      # Importo in centesimi di euro
    description: str       # Causale del pagamento
    citizen_fiscal_code: str
    due_date: datetime
    company_name: str      # Denominazione dell'Ente Creditore

class PagoPAClient:
    """
    Client per le API del Nodo pagoPA (versione REST/JSON).
    Supporta le API GPD (Gestione Posizioni Debitorie).
    """

    def __init__(self, organization_fiscal_code: str, api_key: str, base_url: str):
        self.org_fc = organization_fiscal_code
        self.api_key = api_key
        self.base_url = base_url

    def generate_iuv(self) -> str:
        """
        Genera un IUV conforme alle specifiche pagoPA.
        Struttura per Enti con aux digit 3 (applicativo gestionale):
        - 17 caratteri numerici
        - Deve essere univoco per l'Ente Creditore
        """
        # Componente temporale: YYMMDDHHMM (10 cifre)
        time_component = datetime.utcnow().strftime("%y%m%d%H%M")
        # Componente random: 7 cifre
        random_component = str(uuid.uuid4().int)[:7]
        iuv = f"{time_component}{random_component}"
        return iuv[:17]  # Tronca a 17 caratteri

    async def create_payment_position(self, position: PaymentPosition) -> dict:
        """
        Crea una posizione debitoria sul nodo pagoPA.
        """
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/organizations/{self.org_fc}/debtpositions",
                headers={
                    "Ocp-Apim-Subscription-Key": self.api_key,
                    "Content-Type": "application/json"
                },
                json={
                    "iupd": f"{self.org_fc}-{position.iuv}",  # Identificativo Univoco Posizione Debitoria
                    "type": "F",                          # F = Persona fisica
                    "fiscalCode": position.citizen_fiscal_code,
                    "companyName": position.company_name,
                    "validityDate": position.due_date.isoformat(),
                    "paymentOption": [
                        {
                            "iuv": position.iuv,
                            "amount": position.amount_cents,
                            "description": position.description,
                            "isPartialPayment": False,
                            "dueDate": (position.due_date + timedelta(days=30)).isoformat(),
                            "fee": 100,  # Commissione in centesimi (1 euro)
                            "transfer": [
                                {
                                    "idTransfer": "1",
                                    "amount": position.amount_cents,
                                    "organizationFiscalCode": self.org_fc,
                                    "remittanceInformation": position.description,
                                    "category": "0201102IM",  # Codice tassonomia
                                }
                            ]
                        }
                    ]
                }
            )
            response.raise_for_status()
            return response.json()

    def generate_payment_notice_url(self, iuv: str) -> str:
        """
        Genera il link di pagamento che il cittadino può usare.
        Formato standard: https://checkout.pagopa.it/pay?...
        """
        notice_number = f"3{self.org_fc}{iuv}"  # Numero Avviso pagoPA
        return (
            f"https://checkout.pagopa.it/pay"
            f"?rptId={self.org_fc}{notice_number}"
            f"&amount={100}"  # Amount in centesimi
        )

テストおよび開発環境

AgID と PagoPA は、開発と検証のための専用のテスト環境を提供します。

システム 環境 URL 資格証明テスト
SPID OIDC デモ/テスト デモ.spid.gov.it Developers.italia.it でユーザーをテストする
CIE OIDC テスト preprod.cie.gov.it MinInterno ポータル経由でリクエストする
SPID SAML IdP テスト spidtest.agid.gov.it テスト/テスト (レベル 1、2、3)
PayPA GPD UAT api.uat.platform.pagopa.it devOps ポータルでリクエストされた API キー
パゴPAチェックアウト UAT uat.checkout.pagopa.it テストカード: 4242 4242 4242 4242

公式SDKと推奨ライブラリ

プロジェクト 開発者イタリア (developers.italia.it) は統合用の公式 SDK を維持します 5 つのプログラミング言語の SPID と CIE:

  • パイソン: spid-cie-oidc-django (Django に基づく)、 pyspid (SAML)
  • ジャワ: spid-spring-integration (スプリングブーツ)
  • 。ネット: spid-dotnet-sdk (ASP.NETコア)
  • PHP: spid-php
  • ルビー: spid-ruby

公式 SDK でカバーされていないコンテキストでの統合については、完全な技術仕様が利用可能です docs.italia.it (「SPID CIE OIDC」または「SPID Technical Rules」を検索)。 OIDC フェデレーションは次のことができます。 任意の標準 OIDC ライブラリで実装され、SPID/CIE 固有のクレームのサポートが追加されます。

結論と次のステップ

SPID、CIE、pagoPA をイタリアのデジタル サービスに統合することは、次のような PA の要件です。 CAD 準拠のオンライン サービスを提供したいと考えています。 OpenID Connect への移行が大幅に簡素化 過去の SAML 2.0 と比較した実装、および Developers Italia の公式 SDK はさらに低い 参入障壁。

このシリーズの次の最後の記事では、 GovStack ビルディング ブロック: フレームワーク 再利用可能なデジタル政府サービスを構築するための国際モジュラー システム。2025 年までに 20 か国以上で採用される。

このシリーズの関連記事

  • ガバテック #00: デジタル公共インフラ - ビルディングブロックとグローバルアーキテクチャ
  • ガバメントテック #01: eIDAS 2.0 および EUDI ウォレット - ヨーロッパのデジタル ID
  • ガバメントテック #02: OpenID Connect for Government Identity - 完全展開
  • ガバメントテック #04: GDPR-by-Design - PA サービスにおけるユーザー データとプライバシー