Get your free and exclusive 80-page Banking Passkey Report
Back to Overview

Come creare un Verifier di credenziali digitali (Guida per sviluppatori)

Impara a creare da zero un verifier di credenziali digitali usando Next.js, OpenID4VP e ISO mDoc. Questa guida passo-passo per sviluppatori mostra come creare un verifier in grado di richiedere, ricevere e convalidare patenti di guida mobili e altre crede

Amine

Created: August 20, 2025

Updated: August 21, 2025

Blog-Post-Header-Image

See the original blog version in English here.

DigitalCredentialsDemo Icon

Want to experience digital credentials in action?

Try Digital Credentials

1. Introduzione#

Dimostrare la propria identità online è una sfida costante, che porta a fare affidamento sulle password e alla condivisione di documenti sensibili su canali non sicuri. Questo ha reso la verifica dell'identità per le aziende un processo lento, costoso e soggetto a frodi. Le credenziali digitali offrono un nuovo approccio, ridando agli utenti il controllo dei propri dati. Sono l'equivalente digitale di un wallet fisico, contenente di tutto, dalla patente di guida alla laurea, ma con i vantaggi aggiuntivi di essere crittograficamente sicure, rispettose della privacy e verificabili all'istante.

Questa guida offre agli sviluppatori un tutorial pratico e passo-passo per costruire un verifier per le credenziali digitali. Sebbene gli standard esistano, ci sono poche indicazioni su come implementarli. Questo tutorial colma questa lacuna, mostrando come costruire un verifier utilizzando l'API nativa per le credenziali digitali del browser, OpenID4VP per il protocollo di presentazione e ISO mDoc (ad es., la patente di guida mobile) come formato della credenziale.

Il risultato finale sarà un'applicazione Next.js semplice ma funzionale, in grado di richiedere, ricevere e verificare una credenziale digitale da un wallet mobile compatibile.

Ecco una rapida anteprima dell'applicazione finale in azione. Il processo prevede quattro passaggi principali:

Passaggio 1: Pagina iniziale L'utente arriva sulla pagina iniziale e clicca su "Verifica con identità digitale" per avviare il processo.

Passaggio 2: Prompt di fiducia Il browser chiede all'utente di dare fiducia. L'utente clicca su "Continua" per procedere.

Passaggio 3: Scansione del codice QR Viene visualizzato un codice QR, che l'utente scansiona con la sua applicazione wallet compatibile.

Passaggio 4: Credenziale decodificata Dopo la verifica riuscita, l'applicazione mostra i dati della credenziale decodificata.

1.1 Come funziona#

La magia dietro le credenziali digitali risiede in un modello a "triangolo della fiducia" semplice ma potente, che coinvolge tre attori chiave:

  • Issuer: Un'autorità fidata (ad es., un'agenzia governativa, un'università o una banca) che firma crittograficamente ed emette una credenziale a un utente.
  • Holder: L'utente, che riceve la credenziale e la conserva in modo sicuro in un wallet digitale personale sul proprio dispositivo.
  • Verifier: Un'applicazione o un servizio che deve controllare la credenziale dell'utente.

Quando un utente vuole accedere a un servizio, presenta la credenziale dal suo wallet. Il verifier può quindi verificarne istantaneamente l'autenticità senza dover contattare direttamente l'issuer originale.

1.2 Perché i verifier sono essenziali (e perché sei qui)#

Perché questo ecosistema di identità decentralizzata possa prosperare, il ruolo del verifier è assolutamente cruciale. Sono i guardiani di questa nuova infrastruttura di fiducia, coloro che consumano le credenziali e le rendono utili nel mondo reale. Come illustra il diagramma sottostante, un verifier completa il triangolo della fiducia richiedendo, ricevendo e convalidando una credenziale dal holder.

Se sei uno sviluppatore, costruire un servizio per eseguire questa verifica è una competenza fondamentale per la prossima generazione di applicazioni sicure e incentrate sull'utente. Questa guida è progettata per accompagnarti esattamente in questo processo. Tratteremo tutto ciò che devi sapere per costruire il tuo verifier di credenziali verificabili, dai concetti e standard di base ai dettagli di implementazione passo-passo per la convalida delle firme e il controllo dello stato delle credenziali.

Vuoi andare avanti? Puoi trovare il progetto completo di questo tutorial su GitHub. Sentiti libero di clonarlo e provarlo tu stesso: https://github.com/corbado/digital-credentials-example

Iniziamo.

2. Prerequisiti per costruire un Verifier#

Prima di iniziare, assicurati di avere:

  1. Comprensione di base delle credenziali digitali e di mdoc
    • Questo tutorial si concentra sul formato ISO mDoc (ad es., per le patenti di guida mobili) e non tratta altri formati come le credenziali verificabili (VC) del W3C. Una certa familiarità con i concetti di base di mdoc sarà utile.
  2. Docker e Docker Compose
    • Il nostro progetto utilizza un database MySQL in un container Docker per gestire lo stato della sessione OIDC. Assicurati di averli entrambi installati e in esecuzione.
  3. Protocollo scelto: OpenID4VP
    • Useremo il protocollo OpenID4VP (OpenID for Verifiable Presentations) per il flusso di scambio delle credenziali.
  4. Stack tecnologico pronto
    • Useremo TypeScript (Node.js) per la logica di backend.
    • Useremo Next.js sia per il backend (API routes) che per il frontend (UI).
    • Librerie chiave: librerie di decodifica CBOR per il parsing di mdoc e un client MySQL.
  5. Credenziali di test e Wallet
  6. Conoscenze di base di crittografia
    • Comprendere le firme digitali e i concetti di chiave pubblica/privata in relazione a mdoc e ai flussi OIDC.

Ora esamineremo in dettaglio ciascuno di questi prerequisiti, iniziando dagli standard e protocolli che sono alla base di questo verifier basato su mdoc.

2.1 Scelte del protocollo#

Il nostro verifier è costruito per quanto segue:

Standard / ProtocolloDescrizione
W3C VCIl W3C Verifiable Credentials Data Model. Definisce la struttura standard per le credenziali digitali, inclusi claim, metadati e prove.
SD-JWTSelective Disclosure per JWT. Un formato per VC basato su JSON Web Token che consente ai titolari di divulgare selettivamente solo specifici claim da una credenziale, migliorando la privacy.
ISO mDocISO/IEC 18013-5. Lo standard internazionale per le patenti di guida mobili (mDL) e altri documenti d'identità mobili, che definisce le strutture dati e i protocolli di comunicazione per l'uso offline e online.
OpenID4VPOpenID for Verifiable Presentations. Un protocollo di presentazione interoperabile basato su OAuth 2.0. Definisce come un verifier richiede le credenziali e come il wallet di un holder le presenta.

Per questo tutorial, usiamo specificamente:

  • OpenID4VP come protocollo per richiedere e ricevere le credenziali.
  • ISO mDoc come formato della credenziale (ad es., per le patenti di guida mobili).

Nota sull'ambito: Sebbene introduciamo brevemente W3C VC e SD-JWT per fornire un contesto più ampio, questo tutorial implementa esclusivamente credenziali ISO mDoc tramite OpenID4VP. Le VC basate su W3C sono fuori dall'ambito di questo esempio.

2.1.1 ISO mDoc (Mobile Document)#

Lo standard ISO/IEC 18013-5 mDoc definisce la struttura e la codifica per i documenti mobili come le patenti di guida mobili (mDL). Le credenziali mDoc sono codificate in CBOR, firmate crittograficamente e possono essere presentate digitalmente per la verifica. Il nostro verifier si concentrerà sulla decodifica e la convalida di queste credenziali mdoc.

2.1.2 OpenID4VP (OpenID for Verifiable Presentations)#

OpenID4VP è un protocollo interoperabile per la richiesta e la presentazione di credenziali digitali, basato su OAuth 2.0 e OpenID Connect. In questa implementazione, OpenID4VP viene utilizzato per:

  • Avviare il flusso di presentazione della credenziale (tramite codice QR o API del browser)
  • Ricevere la credenziale mdoc dal wallet dell'utente
  • Garantire uno scambio di credenziali sicuro, stateful e rispettoso della privacy

2.2 Scelte dello stack tecnologico#

Ora che abbiamo una chiara comprensione degli standard e dei protocolli, dobbiamo scegliere lo stack tecnologico giusto per costruire il nostro verifier. Le nostre scelte sono progettate per garantire robustezza, una buona esperienza per lo sviluppatore e compatibilità con il moderno ecosistema web.

2.2.1 Linguaggio: TypeScript#

Useremo TypeScript sia per il nostro codice frontend che backend. Essendo un soprainsieme di JavaScript, aggiunge la tipizzazione statica, che aiuta a individuare gli errori precocemente, migliora la qualità del codice e rende più facili da gestire le applicazioni complesse. In un contesto sensibile alla sicurezza come la verifica delle credenziali, la sicurezza dei tipi è un enorme vantaggio.

2.2.2 Framework: Next.js#

Next.js è il nostro framework di scelta perché offre un'esperienza integrata e fluida per la creazione di applicazioni full-stack.

  • Per il frontend: Useremo Next.js con React per costruire l'interfaccia utente in cui viene avviato il processo di verifica (ad es., visualizzando un codice QR).
  • Per il backend: Sfrutteremo le API Routes di Next.js per creare gli endpoint lato server. Questi endpoint sono responsabili della creazione di richieste OpenID4VP valide e di agire come redirect_uri per ricevere e verificare in modo sicuro la risposta finale dal CMWallet.

2.2.3 Librerie chiave#

La nostra implementazione si basa su un insieme specifico di librerie per il frontend e il backend:

  • next: Il framework Next.js, utilizzato sia per le API routes del backend che per l'interfaccia utente del frontend.
  • react e react-dom: Alimentano l'interfaccia utente del frontend.
  • cbor-web: Utilizzato per decodificare le credenziali mdoc codificate in CBOR in oggetti JavaScript utilizzabili.
  • mysql2: Fornisce la connettività al database MySQL per la memorizzazione delle challenge e delle sessioni di verifica.
  • uuid: Una libreria per la generazione di stringhe di challenge (nonce) uniche.
  • @types/uuid: Tipi TypeScript per la generazione di UUID.

Nota su openid-client: Verifier più avanzati e di livello produttivo potrebbero utilizzare la libreria openid-client per gestire direttamente il protocollo OpenID4VP sul backend, abilitando funzionalità come un redirect_uri dinamico. In un flusso OpenID4VP guidato dal server con un redirect_uri, openid-client verrebbe utilizzato per analizzare e convalidare direttamente le risposte vp_token. Per questo tutorial, stiamo usando un flusso più semplice, mediato dal browser, che non lo richiede, rendendo il processo più facile da capire.

Questo stack tecnologico garantisce un'implementazione del verifier robusta, type-safe e scalabile, focalizzata sull'API per le credenziali digitali del browser e sul formato delle credenziali ISO mDoc.

2.3 Ottenere un wallet e credenziali di test#

Per testare il tuo verifier, hai bisogno di un wallet mobile in grado di interagire con l'API per le credenziali digitali del browser.

Useremo il CMWallet, un robusto wallet di test compatibile con OpenID4VP per Android.

Come installare CMWallet (Android):

  1. Scarica il file APK utilizzando il link sopra direttamente sul tuo dispositivo Android.
  2. Apri le Impostazioni > Sicurezza del tuo dispositivo.
  3. Abilita "Installa app sconosciute" per il browser che hai utilizzato per scaricare il file.
  4. Individua l'APK scaricato nella cartella "Download" e toccalo per avviare l'installazione.
  5. Segui le istruzioni sullo schermo per completare l'installazione.
  6. Apri CMWallet e lo troverai precaricato con credenziali di test, pronto per il flusso di verifica.

Nota: Installa file APK solo da fonti di cui ti fidi. Il link fornito proviene dal repository ufficiale del progetto.

2.4 Conoscenze di crittografia#

Prima di immergerci nell'implementazione, è essenziale comprendere i concetti crittografici che sono alla base delle credenziali verificabili. È questo che le rende "verificabili" e affidabili.

2.4.1 Firme digitali: il fondamento della fiducia#

Nel suo nucleo, una credenziale verificabile è un insieme di attestazioni (come nome, data di nascita, ecc.) che è stato firmato digitalmente da un issuer. Una firma digitale offre due garanzie cruciali:

  • Autenticità: Dimostra che la credenziale è stata effettivamente creata dall'issuer e non da un impostore.
  • Integrità: Dimostra che la credenziale non è stata alterata o manomessa da quando è stata firmata.

2.4.2 Crittografia a chiave pubblica/privata#

Le firme digitali vengono create utilizzando la crittografia a chiave pubblica/privata (chiamata anche crittografia asimmetrica). Ecco come funziona nel nostro contesto:

  1. L'Issuer ha una coppia di chiavi: una chiave privata, che viene mantenuta segreta, e una chiave pubblica, che viene resa disponibile a tutti (solitamente tramite il loro DID Document).
  2. Firma: Quando un issuer crea una credenziale, utilizza la sua chiave privata per generare una firma digitale unica per quei dati specifici della credenziale.
  3. Verifica: Quando il nostro verifier riceve la credenziale, utilizza la chiave pubblica dell'issuer per controllare la firma. Se il controllo ha successo, il verifier sa che la credenziale è autentica e non è stata manomessa. Qualsiasi modifica ai dati della credenziale invaliderebbe la firma.

Nota sui DID: In questo tutorial, non risolviamo le chiavi dell'issuer tramite DID. In produzione, gli issuer esporrebbero tipicamente le chiavi pubbliche tramite DID o altri endpoint autorevoli, che il verifier utilizzerebbe per la convalida crittografica.

2.4.3 Credenziali verificabili come JWT#

Le credenziali verificabili sono spesso formattate come JSON Web Token (JWT). Un JWT è un modo compatto e sicuro per gli URL di rappresentare le attestazioni da trasferire tra due parti. Un JWT firmato (noto anche come JWS) ha tre parti separate da punti (.):

  • Header: Contiene metadati sul token, come l'algoritmo di firma utilizzato (alg).
  • Payload: Contiene le attestazioni effettive della credenziale verificabile (claim vc), inclusi issuer, credentialSubject, ecc.
  • Signature: La firma digitale generata dall'issuer, che copre l'header e il payload.
// Esempio di una struttura JWT [Header].[Payload].[Signature]

Nota: Le credenziali verificabili basate su JWT sono fuori dall'ambito di questo post del blog. Questa implementazione si concentra sulle credenziali ISO mDoc e OpenID4VP, non sulle credenziali verificabili W3C o basate su JWT.

2.4.4 La presentazione verificabile: provare il possesso#

Non è sufficiente per un verifier sapere che una credenziale è valida; deve anche sapere che la persona che presenta la credenziale è il legittimo titolare. Questo impedisce a qualcuno di usare una credenziale rubata.

Questo problema viene risolto utilizzando una presentazione verificabile (VP). Una VP è un involucro attorno a una o più VC che è firmato dal titolare stesso.

Il flusso è il seguente:

  1. Il verifier chiede all'utente di presentare una credenziale.
  2. Il wallet dell'utente crea una presentazione verificabile, raggruppa al suo interno le credenziali richieste e firma l'intera presentazione utilizzando la chiave privata del titolare.
  3. Il wallet invia questa VP firmata al verifier.

Il nostro verifier deve quindi eseguire due controlli di firma separati:

  1. Verificare la/le credenziale/i: Controllare la firma su ogni VC all'interno della presentazione utilizzando la chiave pubblica dell'issuer. (Dimostra che la credenziale è reale).
  2. Verificare la presentazione: Controllare la firma sulla VP stessa utilizzando la chiave pubblica del titolare. (Dimostra che la persona che la presenta è il proprietario).

Questo controllo a due livelli garantisce sia l'autenticità della credenziale che l'identità della persona che la presenta, creando un modello di fiducia robusto e sicuro.

Nota: Il concetto di presentazioni verificabili come definito nell'ecosistema W3C VC è fuori dall'ambito di questo post del blog. Il termine presentazione verificabile qui si riferisce alla risposta OpenID4VP vp_token, che si comporta in modo simile a una VP W3C ma si basa sulla semantica ISO mDoc piuttosto che sul modello di firma JSON-LD del W3C. Questa guida si concentra sulle credenziali ISO mDoc e OpenID4VP, non sulle presentazioni verificabili W3C o sulla loro validazione della firma.

3. Panoramica dell'architettura#

La nostra architettura del verifier utilizza l'API per le credenziali digitali integrata nel browser come intermediario sicuro per connettere la nostra applicazione web con il CMWallet mobile dell'utente. Questo approccio semplifica il flusso lasciando che sia il browser a gestire la visualizzazione nativa del codice QR e la comunicazione con il wallet.

  • Frontend (Next.js & React): Un sito web leggero rivolto all'utente. Il suo compito è recuperare un oggetto di richiesta dal nostro backend, passarlo all'API navigator.credentials.get() del browser, ricevere il risultato e inoltrarlo al nostro backend per la verifica.
  • Backend (API Routes di Next.js): Il motore del verifier. Genera un oggetto di richiesta valido per l'API del browser ed espone un endpoint per ricevere la presentazione della credenziale dal frontend per la convalida finale.
  • Browser (API delle credenziali): Il facilitatore. Riceve l'oggetto di richiesta dal nostro frontend, comprende il protocollo openid4vp e genera nativamente un codice QR. Attende quindi che il wallet restituisca una risposta.
  • CMWallet (App mobile): Il wallet dell'utente. Scansiona il codice QR, elabora la richiesta, ottiene il consenso dell'utente e invia la risposta firmata al browser.

Ecco un diagramma di sequenza che illustra il flusso completo e accurato:

Spiegazione del flusso:

  1. Avvio: L'utente clicca sul pulsante "Verifica" sul nostro frontend.
  2. Oggetto di richiesta: Il frontend chiama il nostro backend (/api/verify/start), che genera un oggetto di richiesta contenente la query e un nonce, quindi lo restituisce.
  3. Chiamata API del browser: Il frontend chiama navigator.credentials.get() con l'oggetto di richiesta.
  4. Codice QR nativo: Il browser vede la richiesta del protocollo openid4vp e visualizza nativamente un codice QR. La promessa .get() è ora in attesa.

Nota: Questo flusso con codice QR avviene sui browser desktop. Sui browser mobili (Android Chrome con flag sperimentale abilitato), il browser può comunicare direttamente con i wallet compatibili sullo stesso dispositivo, eliminando la necessità di scansionare il codice QR. Per abilitare questa funzione su Android Chrome, vai su chrome://flags#web-identity-digital-credentials e imposta il flag su "Enabled".

  1. Scansione e presentazione: L'utente scansiona il codice QR con CMWallet. Il wallet ottiene l'approvazione dell'utente e invia la presentazione verificabile al browser.
  2. Risoluzione della promessa: Il browser riceve la risposta e la promessa .get() originale sul frontend si risolve finalmente, consegnando il payload della presentazione.
  3. Verifica del backend: Il frontend invia il payload della presentazione tramite POST all'endpoint /api/verify/finish del nostro backend. Il backend convalida il nonce e la credenziale.
  4. Risultato: Il backend restituisce un messaggio finale di successo o fallimento al frontend, che aggiorna l'interfaccia utente.

4. Costruire il Verifier#

Ora che abbiamo una solida comprensione degli standard, dei protocolli e del flusso architetturale, possiamo iniziare a costruire il nostro verifier.

Segui passo passo o usa il codice finale

Ora esamineremo la configurazione e l'implementazione del codice passo dopo passo. Se preferisci passare direttamente al prodotto finito, puoi clonare il progetto completo dal nostro repository GitHub ed eseguirlo localmente.

git clone https://github.com/corbado/digital-credentials-example.git

4.1 Configurazione del progetto#

Innanzitutto, inizializzeremo un nuovo progetto Next.js, installeremo le dipendenze necessarie e avvieremo il nostro database.

4.1.1 Inizializzazione dell'app Next.js#

Apri il terminale, naviga nella directory in cui vuoi creare il tuo progetto ed esegui il seguente comando. Per questo progetto stiamo usando l'App Router, TypeScript e Tailwind CSS.

npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm

Questo comando crea lo scheletro di una nuova applicazione Next.js nella tua directory corrente.

4.1.2 Installazione delle dipendenze#

Successivamente, dobbiamo installare le librerie che gestiranno la decodifica CBOR, le connessioni al database e la generazione di UUID.

npm install cbor-web mysql2 uuid @types/uuid

Questo comando installa:

  • cbor-web: Per la decodifica del payload della credenziale mdoc.
  • mysql2: Il client MySQL per il nostro database.
  • uuid: Per generare stringhe di challenge uniche.
  • @types/uuid: Tipi TypeScript per la libreria uuid.

4.1.3 Avvio del database#

Il nostro backend richiede un database MySQL per memorizzare i dati della sessione OIDC, garantendo che ogni flusso di verifica sia sicuro e stateful. Abbiamo incluso un file docker-compose.yml per rendere questo processo semplice.

Se hai clonato il repository, puoi semplicemente eseguire docker-compose up -d. Se stai costruendo da zero, crea un file chiamato docker-compose.yml con il seguente contenuto:

services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: digital_credentials MYSQL_USER: app_user MYSQL_PASSWORD: app_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 volumes: mysql_data:

Questa configurazione di Docker Compose richiede anche uno script di inizializzazione SQL. Crea una directory chiamata sql e al suo interno un file chiamato init.sql con il seguente contenuto per configurare le tabelle necessarie:

-- Create database if not exists CREATE DATABASE IF NOT EXISTS digital_credentials; USE digital_credentials; -- Table for storing challenges CREATE TABLE IF NOT EXISTS challenges ( id VARCHAR(36) PRIMARY KEY, challenge VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_challenge (challenge), INDEX idx_expires_at (expires_at) ); -- Table for storing verification sessions CREATE TABLE IF NOT EXISTS verification_sessions ( id VARCHAR(36) PRIMARY KEY, challenge_id VARCHAR(36), status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending', presentation_data JSON, verified_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE, INDEX idx_challenge_id (challenge_id), INDEX idx_status (status) ); -- Table for storing verified credentials data (optional) CREATE TABLE IF NOT EXISTS verified_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_type VARCHAR(255), issuer VARCHAR(255), subject VARCHAR(255), claims JSON, verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE, INDEX idx_session_id (session_id), INDEX idx_credential_type (credential_type) );

Una volta che entrambi i file sono al loro posto, apri il terminale nella radice del progetto ed esegui:

docker-compose up -d

Questo comando avvierà un container MySQL in background.

4.2 Panoramica dell'architettura dell'app Next.js#

La nostra applicazione Next.js è strutturata per separare le responsabilità tra frontend e backend, anche se fanno parte dello stesso progetto.

  • Frontend (src/app/page.tsx): Una singola pagina React che avvia il flusso di verifica e visualizza il risultato. Interagisce con l'API per le credenziali digitali del browser.
  • Backend API Routes (src/app/api/verify/...):
    • start/route.ts: Genera la richiesta OpenID4VP e un nonce di sicurezza.
    • finish/route.ts: Riceve la presentazione dal wallet (tramite il browser), convalida il nonce e decodifica la credenziale.
  • Libreria (src/lib/):
    • database.ts: Gestisce tutte le interazioni con il database (creazione di challenge, verifica delle sessioni).
    • crypto.ts: Gestisce la decodifica della credenziale mDoc basata su CBOR.

Ecco un diagramma che illustra l'architettura interna:

4.3 Costruire il frontend#

Il nostro frontend è intenzionalmente leggero. La sua responsabilità principale è agire come trigger rivolto all'utente per il flusso di verifica e comunicare sia con il nostro backend che con le capacità native di gestione delle credenziali del browser. Non contiene alcuna logica di protocollo complessa; tutto ciò è delegato.

Nello specifico, il frontend gestirà quanto segue:

  • Interazione con l'utente: Fornisce un'interfaccia semplice, come un pulsante "Verifica", per consentire all'utente di avviare il processo.
  • Gestione dello stato: Gestisce lo stato dell'interfaccia utente, mostrando indicatori di caricamento mentre la verifica è in corso e visualizzando il messaggio finale di successo o errore.
  • Comunicazione con il backend (richiesta): Chiama /api/verify/start e riceve un payload JSON strutturato (protocol, request, state) che descrive esattamente cosa il wallet dovrebbe presentare.
  • Invocazione dell'API del browser: Passa quell'oggetto JSON a navigator.credentials.get(), che renderizza un codice QR nativo e attende la risposta del wallet.
  • Comunicazione con il backend (risposta): Una volta che l'API del browser restituisce la presentazione verificabile, invia questi dati al nostro endpoint /api/verify/finish in una richiesta POST per la convalida finale lato server.
  • Visualizzazione dei risultati: Aggiorna l'interfaccia utente per informare l'utente se la verifica è andata a buon fine o è fallita, in base alla risposta del backend.

La logica principale si trova nella funzione startVerification:

// src/app/page.tsx const startVerification = async () => { setLoading(true); setVerificationResult(null); try { // 1. Controlla se il browser supporta l'API if (!navigator.credentials?.get) { throw new Error("Il browser non supporta la Credential API."); } // 2. Chiedi al nostro backend un oggetto di richiesta const res = await fetch("/api/verify/start"); const { protocol, request } = await res.json(); // 3. Passa quell'oggetto al browser – questo attiva il codice QR nativo const credential = await (navigator.credentials as any).get({ mediation: "required", digital: { requests: [ { protocol, // "openid4vp" data: request, // contiene dcql_query, nonce, ecc. }, ], }, }); // 4. Inoltra la risposta del wallet (dal browser) al nostro endpoint di fine per i controlli lato server const verifyRes = await fetch("/api/verify/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credential), }); const result = await verifyRes.json(); if (verifyRes.ok && result.verified) { setVerificationResult(`Successo: ${result.message}`); } else { throw new Error(result.message || "Verifica fallita."); } } catch (err) { setVerificationResult(`Errore: ${(err as Error).message}`); } finally { setLoading(false); } };

Questa funzione mostra i quattro passaggi chiave della logica del frontend: controllo del supporto API, recupero della richiesta dal backend, chiamata all'API del browser e invio del risultato per la verifica. Il resto del file è boilerplate React standard per la gestione dello stato e il rendering dell'interfaccia utente, che puoi visualizzare nel repository GitHub.

Perché digital e mediation: 'required'?#

Potresti notare che la nostra chiamata a navigator.credentials.get() sembra diversa da esempi più semplici. Questo perché stiamo aderendo rigorosamente alla specifica ufficiale dell'API per le credenziali digitali del W3C.

  • Membro digital: La specifica richiede che tutte le richieste di credenziali digitali siano annidate all'interno di un oggetto digital. Questo fornisce uno spazio dei nomi chiaro e standardizzato per questa API, distinguendola da altri tipi di credenziali (come password o federated) e consentendo estensioni future senza conflitti.

  • mediation: 'required': Questa opzione è una caratteristica cruciale per la sicurezza e l'esperienza dell'utente. Impone che l'utente debba interagire attivamente con un prompt (ad es., una scansione biometrica, l'inserimento di un PIN o una schermata di consenso) per approvare la richiesta di credenziali. Senza di essa, un sito web potrebbe potenzialmente tentare di accedere silenziosamente alle credenziali in background, il che rappresenta un rischio significativo per la privacy. Richiedendo la mediazione, garantiamo che l'utente sia sempre in controllo e dia il consenso esplicito per ogni transazione.

4.4 Costruire gli endpoint del backend#

Con l'interfaccia utente React pronta, ora abbiamo bisogno di due API routes che svolgano il lavoro pesante sul server:

  1. /api/verify/start – costruisce una richiesta OpenID4VP, salva una challenge monouso in MySQL e restituisce tutto al browser.
  2. /api/verify/finish – riceve la risposta del wallet, convalida la challenge, verifica e decodifica la credenziale e infine restituisce un risultato JSON conciso all'interfaccia utente.

4.4.1 /api/verify/start: Generare la richiesta OpenID4VP#

// src/app/api/verify/start/route.ts import { NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createChallenge, cleanupExpiredChallenges } from "@/lib/database"; export async function GET() { // 1️⃣ Crea un nonce (challenge) casuale e di breve durata const challenge = uuidv4(); const challengeId = uuidv4(); const expiresAt = new Date(Date.now() + 5 * 60 * 1000); await createChallenge(challengeId, challenge, expiresAt); cleanupExpiredChallenges().catch(console.error); // 2️⃣ Costruisci una query DCQL che descrive *cosa* vogliamo const dcqlQuery = { credentials: [ { id: "cred1", format: "mso_mdoc", meta: { doctype_value: "eu.europa.ec.eudi.pid.1" }, claims: [ { path: ["eu.europa.ec.eudi.pid.1", "family_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "given_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "birth_date"] }, ], }, ], }; // 3️⃣ Restituisci un oggetto che il browser può passare a navigator.credentials.get() return NextResponse.json({ protocol: "openid4vp", // dice al browser quale protocollo di wallet usare request: { dcql_query: dcqlQuery, // COSA presentare nonce: challenge, // anti-replay response_type: "vp_token", response_mode: "dc_api", // il wallet farà una POST direttamente a /finish }, state: { credential_type: "mso_mdoc", // conservato per controlli successivi nonce: challenge, challenge_id: challengeId, }, }); }

Parametri chiave

noncesfida crittografica che lega richiesta e risposta (previene il replay). • dcql_query – Un oggetto che descrive le attestazioni esatte di cui abbiamo bisogno. Per questa guida, usiamo una struttura dcql_query ispirata alle recenti bozze del Digital Credential Query Language, anche se non è ancora uno standard finalizzato. • state – JSON arbitrario restituito dal wallet in modo che possiamo cercare il record nel database.

4.4.2 Helper del database#

Il file src/lib/database.ts racchiude le operazioni MySQL di base per le challenge e le sessioni di verifica (inserimento, lettura, contrassegno come usato). Mantenere questa logica in un unico modulo rende facile sostituire il datastore in seguito.


4.5 /api/verify/finish: Convalidare e decodificare la presentazione#

// src/app/api/verify/finish/route.ts import { NextResponse, NextRequest } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getChallenge, markChallengeAsUsed, createVerificationSession, updateVerificationSession, } from "@/lib/database"; import { decodeDigitalCredential, decodeAllNamespaces } from "@/lib/crypto"; export async function POST(request: NextRequest) { const body = await request.json(); // 1️⃣ Estrai i pezzi della presentazione verificabile const vpTokenMap = body.vp_token ?? body.data?.vp_token; const state = body.state; const mdocToken = vpTokenMap?.cred1; // abbiamo richiesto questo ID in dcqlQuery if (!vpTokenMap || !state || !mdocToken) { return NextResponse.json( { verified: false, message: "Risposta malformata" }, { status: 400 }, ); } // 2️⃣ Validazione della challenge monouso const stored = await getChallenge(state.nonce); if (!stored) { return NextResponse.json( { verified: false, message: "Challenge non valida o scaduta" }, { status: 400 }, ); } const sessionId = uuidv4(); await createVerificationSession(sessionId, stored.id); // 3️⃣ Controlli crittografici (pseudo) – sostituire con una vera validazione mDL in produzione // In un'applicazione reale, useresti una libreria dedicata per eseguire una validazione crittografica completa // della firma mdoc rispetto alla chiave pubblica dell'issuer. const isValid = mdocToken.length > 0; if (!isValid) { await updateVerificationSession(sessionId, "failed", { reason: "validazione mdoc fallita", }); return NextResponse.json( { verified: false, message: "Validazione della credenziale fallita" }, { status: 400 }, ); } // 4️⃣ Decodifica il payload del mobile-DL (mdoc) in JSON leggibile const decoded = await decodeDigitalCredential(mdocToken); const readable = decodeAllNamespaces(decoded)["eu.europa.ec.eudi.pid.1"]; await markChallengeAsUsed(state.nonce); await updateVerificationSession(sessionId, "verified", { readable }); return NextResponse.json({ verified: true, message: "Credenziale mdoc verificata con successo!", credentialData: readable, sessionId, }); }

Campi importanti nella risposta del wallet

vp_token – mappa che contiene ciascuna credenziale restituita dal wallet. Per la nostra demo prendiamo vp_token.cred1. • state – eco del blob che abbiamo fornito in /start; contiene il nonce in modo da poter cercare il record nel database. • mdocToken – una struttura CBOR codificata in Base64URL che rappresenta l'ISO mDoc.

4.6 Decodifica della credenziale mdoc#

Quando il verifier riceve una credenziale mdoc dal browser, si tratta di una stringa Base64URL contenente dati binari codificati in CBOR. Per estrarre le attestazioni effettive, l'endpoint finish esegue un processo di decodifica a più passaggi utilizzando funzioni di supporto da src/lib/crypto.ts.

4.6.1 Passaggio 1: Decodifica Base64URL e CBOR#

La funzione decodeDigitalCredential gestisce la conversione dalla stringa codificata a un oggetto utilizzabile:

// src/lib/crypto.ts export async function decodeDigitalCredential(encodedCredential: string) { // 1. Converti Base64URL in Base64 standard const base64UrlToBase64 = (input: string) => { let base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const pad = base64.length % 4; if (pad) base64 += "=".repeat(4 - pad); return base64; }; const base64 = base64UrlToBase64(encodedCredential); // 2. Decodifica Base64 in binario const binaryString = atob(base64); const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); // 3. Decodifica CBOR const decoded = await cbor.decodeFirst(byteArray); return decoded; }
  • Da Base64URL a Base64: Converte la credenziale da Base64URL a codifica Base64 standard.
  • Da Base64 a binario: Decodifica la stringa Base64 in un array di byte binario.
  • Decodifica CBOR: Utilizza la libreria cbor-web per decodificare i dati binari in un oggetto JavaScript strutturato.

4.6.2 Passaggio 2: Estrazione delle attestazioni con namespace#

La funzione decodeAllNamespaces elabora ulteriormente l'oggetto CBOR decodificato per estrarre le attestazioni effettive dai namespace pertinenti:

// src/lib/crypto.ts export function decodeAllNamespaces(jsonObj) { const decoded = {}; try { jsonObj.documents.forEach((doc, idx) => { // 1) issuerSigned.nameSpaces: const issuerNS = doc.issuerSigned?.nameSpaces || {}; Object.entries(issuerNS).forEach(([nsName, entries]) => { if (!decoded[nsName]) decoded[nsName] = {}; (entries as any[]).forEach((entry) => { const bytes = Uint8Array.from(entry.value); const decodedEntry = cbor.decodeFirstSync(bytes); Object.assign(decoded[nsName], decodedEntry); }); }); // 2) deviceSigned.nameSpaces (se presente): const deviceNS = doc.deviceSigned?.nameSpaces; if (deviceNS?.value?.data) { const bytes = Uint8Array.from(deviceNS.value); decoded[`deviceSigned_ns_${idx}`] = cbor.decodeFirstSync(bytes); } }); } catch (e) { console.error(e); } return decoded; }
  • Itera su tutti i documenti nella credenziale decodificata.
  • Decodifica ogni namespace (ad es., eu.europa.ec.eudi.pid.1) per estrarre i valori effettivi delle attestazioni (come nome, data di nascita, ecc.).
  • Gestisce sia i namespace firmati dall'issuer che quelli firmati dal dispositivo, se presenti.

Esempio di output#

Dopo aver eseguito questi passaggi, l'endpoint finish ottiene un oggetto leggibile dall'uomo contenente le attestazioni dal mdoc, ad esempio:

{ "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" }

Questo processo garantisce che il verifier possa estrarre in modo sicuro e affidabile le informazioni necessarie dalla credenziale mdoc per la visualizzazione e l'ulteriore elaborazione.

4.7 Visualizzazione del risultato nell'interfaccia utente#

L'endpoint finish restituisce un oggetto JSON minimo al frontend:

{ "verified": true, "message": "Credenziale mdoc verificata con successo!", "credentialData": { "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" } }

Il frontend riceve questa risposta in startVerification() e la salva semplicemente nello stato di React in modo da poter renderizzare una bella scheda di conferma o visualizzare singole attestazioni – ad es. “Benvenuto, John Doe (nato il 1990-01-01)!”.

5. Eseguire il Verifier e passi successivi#

Ora hai un verifier completo e funzionante che utilizza le capacità native di gestione delle credenziali del browser. Ecco come eseguirlo localmente e cosa puoi fare per portarlo da una proof-of-concept a un'applicazione pronta per la produzione.

5.1 Come eseguire l'esempio#

  1. Clona il repository:

    git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
  2. Installa le dipendenze:

    npm install
  3. Avvia il database: Assicurati che Docker sia in esecuzione sulla tua macchina, quindi avvia il container MySQL:

    docker-compose up -d
  4. Esegui l'applicazione:

    npm run dev

    Apri il browser all'indirizzo http://localhost:3000 e dovresti vedere l'interfaccia utente del verifier. Ora puoi usare il tuo CMWallet per scansionare il codice QR e completare il flusso di verifica.

5.2 Passi successivi: dalla demo alla produzione#

Questo tutorial fornisce i mattoni fondamentali per un verifier. Per renderlo pronto per la produzione, dovresti implementare diverse funzionalità aggiuntive:

  • Validazione crittografica completa: L'implementazione attuale utilizza un controllo segnaposto (mdocToken.length > 0). In uno scenario reale, è necessario eseguire una validazione crittografica completa della firma mdoc rispetto alla chiave pubblica dell'issuer (ad es., risolvendo il loro DID o recuperando il loro certificato di chiave pubblica). Per gli standard di risoluzione dei DID, fare riferimento alla specifica W3C DID Resolution.

  • Controllo della revoca dell'issuer: Le credenziali possono essere revocate dall'issuer prima della loro data di scadenza. Un verifier di produzione deve controllare lo stato della credenziale interrogando una lista di revoca o un endpoint di stato fornito dall'issuer. La W3C Verifiable Credentials Status List fornisce lo standard per le liste di revoca delle credenziali.

  • Gestione robusta degli errori e sicurezza: Aggiungi una gestione completa degli errori, la validazione degli input, il rate-limiting sugli endpoint API e assicurati che tutte le comunicazioni avvengano tramite HTTPS (TLS) per proteggere i dati in transito. Le OWASP API Security Guidelines forniscono best practice complete per la sicurezza delle API.

  • Supporto per più tipi di credenziali: Estendi la logica per gestire diversi valori di doctype e formati di credenziali se prevedi di ricevere più della sola credenziale PID dell'identità digitale europea (EUDI). Il W3C Verifiable Credentials Data Model fornisce specifiche complete sul formato delle VC.

5.3 Cosa è fuori dall'ambito di questo tutorial#

Questo esempio è volutamente incentrato sul flusso principale mediato dal browser per renderlo facile da capire. I seguenti argomenti sono considerati fuori dall'ambito:

  • Sicurezza pronta per la produzione: Il verifier è a scopo educativo e manca del rafforzamento necessario per un ambiente live.
  • Credenziali verificabili W3C: Questo tutorial si concentra esclusivamente sul formato ISO mDoc per le patenti di guida mobili. Non copre altri formati popolari come JWT-VC o VC con Linked Data Proofs (LD-Proofs).
  • Flussi OpenID4VP avanzati: Non implementiamo funzionalità OpenID4VP più complesse, come la comunicazione diretta wallet-backend tramite un redirect_uri o la registrazione dinamica del client.

Basandoti su queste fondamenta e incorporando questi passaggi successivi, puoi sviluppare un verifier robusto e sicuro in grado di fidarsi e convalidare le credenziali digitali nelle tue applicazioni.

Conclusione#

Ecco fatto! Con meno di 250 righe di TypeScript, ora abbiamo un verifier end-to-end che:

  1. Pubblica una richiesta per l'API delle credenziali del browser.
  2. Permette a qualsiasi wallet compatibile di fornire una presentazione verificabile.
  3. Convalida la presentazione sul server.
  4. Aggiorna l'interfaccia utente in tempo reale.

In produzione, sostituiresti la validazione segnaposto con controlli completi ISO 18013-5, aggiungeresti controlli di revoca dell'issuer, rate-limiting, audit logging e, naturalmente, TLS end-to-end, ma i mattoni fondamentali rimangono esattamente gli stessi.

Risorse#

Ecco alcune delle risorse chiave, specifiche e strumenti utilizzati o citati in questo tutorial:

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook

Table of Contents