{"revision":"8e28655deab6e935","language":"it","entry":{"id":"sign-in","group":"guides","kind":"guide","title":"Accedi con Veil","summary":"Fai entrare le persone nella tua app con la loro scheda Veil. L'accesso è una firma della loro chiave di dispositivo, e puoi controllarla da solo sul registro pubblico.","search":"OpenID Connect · OIDC · OAuth 2.0 · PKCE · token ID · claim · veil_consent · /oidc/authorize","pageLinks":[{"anchor":"how-a-sign-in-works","label":"Flusso"},{"anchor":"endpoints","label":"Endpoint"},{"anchor":"registering-an-app","label":"Registra"},{"anchor":"the-authorization-request","label":"Richiesta"},{"anchor":"try-the-flow","label":"Prova"},{"anchor":"scopes-and-claims","label":"Claim"},{"anchor":"tokens","label":"Token"},{"anchor":"what-to-verify","label":"Verifica"},{"anchor":"honest-limits","label":"Limiti"},{"anchor":"example-node","label":"Esempi"},{"anchor":"reference","label":"Riferimento"}],"sections":[{"kind":"paragraph","text":"Veil è la rubrica pubblica delle identità crittografiche. Ogni persona o organizzazione ha una scheda che lega le sue chiavi agli account con cui la gente la conosce (Discord, GitHub, email, un dominio, il Fediverso e altri), con legami certificati. Accedi con Veil è un provider OpenID Connect costruito su questa rubrica. Quando qualcuno entra nella tua app, l'identità che ricevi è la sua scheda, e la prova è una firma fatta da una chiave che non ha mai lasciato il suo browser."},{"kind":"paragraph","text":"Se costruisci l'app dall'altra parte dell'accesso, qui trovi come registrarla, il flusso, i claim e i token, e cosa puoi controllare da solo sul registro pubblico invece di fidarti della parola del server. L'issuer è `https://veil.rip`."},{"kind":"heading","text":"Come funziona un accesso"},{"kind":"steps","items":["La tua app manda il browser a `https://veil.rip/oidc/authorize` con i soliti parametri del flusso del codice e il `code_challenge` di PKCE.","La pagina di consenso mostra il nome della tua app, il suo sito, cosa chiede e la scheda che questo browser tiene. Nel browser una scheda è una chiave di dispositivo: una chiave Ed25519 creata lì e tenuta in IndexedDB, che nessuna pagina può leggere o esportare. In nessun momento c'è una password.","Quando la persona accetta, il browser firma un testo di consenso con la chiave di dispositivo. Il testo nomina la tua app, il tuo redirect URI, gli scope, il tuo nonce, un hash del tuo state, il tuo `code_challenge` PKCE e l'ora.","Veil controlla quella firma con le chiavi di dispositivo attive della scheda e con la versione corrente della scheda nel registro pubblico verificato. Tiene il consenso firmato insieme all'accesso e rimanda il browser da te con `code`, `state` e `iss`.","Il tuo server scambia il codice con un token di accesso, un refresh token e un token ID firmato da Veil."]},{"kind":"paragraph","text":"Se il browser non ha una scheda, la pagina offre le solite strade: crearne una, o aggiungere questo browser a una scheda che esiste già con il file di backup o con un altro dispositivo. Poi torna alla tua richiesta, che per questo aspetta 30 minuti."},{"kind":"paragraph","text":"La persona approva la tua app una volta. L'accesso dopo, con gli stessi scope, non chiede un clic, ma il browser firma comunque il consenso, quindi ogni accesso è una firma nuova. Per un'app nativa che torna sull'interfaccia di loopback (`http://127.0.0.1`, `[::1]` o `localhost`) la pagina chiede ogni volta, perché qualunque programma sullo stesso computer potrebbe ascoltare a quell'indirizzo (RFC 8252, 8.6)."},{"kind":"paragraph","text":"Sulla pagina del suo account la persona vede le app in cui è entrata, ogni accesso con il suo consenso firmato, e lì può revocare ciascuna di quelle app."},{"kind":"heading","text":"Endpoint"},{"kind":"paragraph","text":"Il documento di discovery, `GET https://veil.rip/.well-known/openid-configuration`, elenca ogni endpoint. Ogni nome qui sotto apre la sua pagina nel riferimento."},{"kind":"table","columns":["Endpoint","Indirizzo"],"rows":[[{"text":"Discovery","ref":"oidc-discovery"},"`https://veil.rip/.well-known/openid-configuration`"],[{"text":"Autorizzazione","ref":"oidc-authorize"},"`https://veil.rip/oidc/authorize`"],[{"text":"Token","ref":"oidc-token"},"`https://veil.rip/oidc/token`"],[{"text":"UserInfo","ref":"oidc-userinfo"},"`https://veil.rip/oidc/userinfo` (GET o POST)"],[{"text":"JWKS","ref":"oidc-jwks"},"`https://veil.rip/oidc/jwks`"],[{"text":"Registrazione (RFC 7591)","ref":"oidc-register"},"`https://veil.rip/oidc/register`"],[{"text":"Revoca (RFC 7009)","ref":"oidc-revoke"},"`https://veil.rip/oidc/revoke`"],[{"text":"Introspezione (RFC 7662)","ref":"oidc-introspect"},"`https://veil.rip/oidc/introspect`"],[{"text":"Fine della sessione","ref":"oidc-logout"},"`https://veil.rip/oidc/logout`"]]},{"kind":"paragraph","text":"Il provider supporta:"},{"kind":"list","items":["Solo il response type `code`, con response mode `query`.","PKCE con `S256` per ogni app, pubblica o confidenziale.","Il parametro `iss` in ogni risposta di autorizzazione (RFC 9207).","Token ID firmati con `EdDSA` (Ed25519) o `ES256`.","L'autenticazione del client con `client_secret_basic` o `private_key_jwt`, e `none` per le app pubbliche."]},{"kind":"paragraph","text":"I request object, `request_uri` e il parametro `claims` non sono supportati."},{"kind":"paragraph","text":"Il documento di discovery e il JWKS rispondono alle richieste da altre origini, e nient'altro sotto `/oidc/` lo fa. Per questo un'app nel browser ha bisogno di un piccolo backend suo che tenga i token e parli con l'endpoint dei token (il modello backend for frontend). Sotto `/oidc/` le chiavi API di Veil non servono: lì l'intestazione `Authorization` porta il segreto della tua app o il token di accesso di una persona."},{"kind":"heading","text":"Registrare un'app"},{"kind":"heading","text":"Da /developers","level":3},{"kind":"paragraph","text":"Su `https://veil.rip/developers` una scheda registra le sue app, con la firma della chiave di dispositivo di quel browser, come tutto il resto della scheda. Le app di un'organizzazione le registra uno dei suoi proprietari. Una scheda può avere 20 app."},{"kind":"table","columns":["Campo","Regole"],"rows":[["Nome","Da 1 a 60 caratteri. La pagina di consenso lo mostra."],["Sito","Un URL https di al più 300 caratteri. La pagina di consenso ne mostra l'host."],["Redirect URI","Da 1 a 10. Ognuno è https, o http sull'interfaccia di loopback (`127.0.0.1`, `[::1]`, `localhost`) per un'app nativa, senza frammento e senza informazioni dell'utente, in al più 512 caratteri ASCII stampabili."],["Redirect URI di logout","Da 0 a 10, con le stesse regole."],["Tipo","`confidential` (un server, con `client_secret_basic` o `private_key_jwt`) o `public` (un'app nativa, solo con PKCE)."],["JWKS","Per `private_key_jwt`, da 1 a 8 chiavi pubbliche: Ed25519, P-256, o RSA da 2048 a 4096 bit con un esponente dispari piccolo (65537, come al solito). Dai le chiavi stesse, mai un URL."],["Algoritmo del token ID","`EdDSA` (quello predefinito) o `ES256`."],["Refresh token","Attivi in modo predefinito."],["Logo","Un PNG o un GIF di al più 256 KiB e 512 × 512 pixel, controllato con le stesse regole dell'immagine di una scheda."]]},{"kind":"paragraph","text":"Quando l'app nasce, Veil mostra il suo client id (`veil_app_` e 32 cifre esadecimali) e, per un server, il suo segreto (`veil_secret_…`). Il segreto si vede solo quella volta, e Veil ne tiene solo l'hash SHA-256. Un segreto nuovo sostituisce subito il vecchio, e cancellare l'app chiude ogni token che ha."},{"kind":"paragraph","text":"Un'app è verificata quando la sua scheda prova, con la prova del dominio della rubrica, il dominio del sito dell'app e di ogni redirect URI https. La pagina di consenso dice allora \"Verificata: il titolare di example.org\". Ogni altra app ha una riga discreta, \"App non verificata\", e la pagina nomina sempre la scheda che l'ha registrata."},{"kind":"heading","text":"Registrazione dinamica","level":3},{"kind":"paragraph","text":"`POST https://veil.rip/oidc/register` è aperto a chiunque (RFC 7591), con questi limiti:"},{"kind":"list","items":["Al più 10 registrazioni l'ora per indirizzo IP.","L'app non appartiene a nessuna scheda, quindi non è mai verificata, e non può chiamarsi Veil.","Scade 30 giorni dopo la registrazione (`client_secret_expires_at` e `veil_expires_at` nella risposta).","`token_endpoint_auth_method` vale `client_secret_basic` se manca, come dice RFC 7591, quindi per un'app pubblica manda `none`.","Per `private_key_jwt` il `jwks` va nella richiesta, `jwks_uri` si rifiuta, e `logo_uri` non si scarica né si tiene."]},{"kind":"code","language":"shell","code":"curl -fsS https://veil.rip/oidc/register -H 'Content-Type: application/json' -d '{\n  \"client_name\": \"Notes CLI\",\n  \"redirect_uris\": [\"http://127.0.0.1/callback\"],\n  \"token_endpoint_auth_method\": \"none\",\n  \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n  \"application_type\": \"native\"\n}'"},{"kind":"heading","text":"La richiesta di autorizzazione"},{"kind":"code","language":"text","code":"https://veil.rip/oidc/authorize?response_type=code\n  &client_id=veil_app_62bab7367bb583e2157490ae2cb65d4d\n  &redirect_uri=https%3A%2F%2Fnotes.example.org%2Fcallback\n  &scope=openid%20profile%20veil\n  &state=…&nonce=…\n  &code_challenge=…&code_challenge_method=S256"},{"kind":"list","items":["`redirect_uri` deve essere uguale a uno di quelli registrati, tranne che un URI di loopback vale con qualunque porta (RFC 8252). Un'app sconosciuta o un redirect URI non registrato si rifiutano sulla pagina di consenso, e il browser non ci viene mai mandato.","`scope` deve contenere `openid`. Uno scope sconosciuto dà `invalid_scope`.","`code_challenge` (43 caratteri) e `code_challenge_method=S256` sono obbligatori per ogni app. Senza, la risposta è `invalid_request`.","`state` arriva a 1024 caratteri e `nonce` a 512. Mandali tutti e due.","`prompt` accetta `none`, `login`, `consent` e `select_account`. Con `none` la risposta è `login_required` quando il browser non ha una scheda, e `consent_required` quando la scheda non ha approvato la tua app per questi scope o il tuo redirect URI è di loopback.","`max_age=0` vale `prompt=login`, quindi la pagina aspetta il clic. Ogni altro `max_age` è già rispettato, perché ogni accesso è una firma fatta in quel momento (`auth_time`).","`ui_locales=it` mostra la pagina in italiano. La lingua predefinita è l'inglese."]},{"kind":"paragraph","text":"La risposta arriva al tuo redirect URI come `code`, `state` e `iss=https://veil.rip`, oppure come `error`, `error_description`, `state` e `iss`. Controlla `state` e `iss` prima di usare il codice."},{"kind":"try-flow"},{"kind":"heading","text":"Scope e claim"},{"kind":"table","columns":["Scope","Claim","Cosa legge la persona"],"rows":[["`openid`","`sub`, e nel token ID `auth_time`, `amr`, `sid`","Il numero della tua scheda"],["`profile`","`name`, `picture`, `preferred_username`, `profile`","Il tuo nome e la tua immagine"],["`veil`","`veil_version`, `veil_log`, `veil_good_standing`, `veil_standing_reasons`, e nel token ID `veil_consent`","Lo stato della tua scheda: se è in regola, la versione su cui si controlla questo accesso e la firma che fai adesso"],["`veil:accounts`","`veil_accounts`","I tuoi account verificati, con la piattaforma e il nome di ciascuno"],["`veil:orgs`","`veil_orgs`","Le tue organizzazioni"],["`veil:keys`","`veil_keys`","Le tue chiavi pubbliche"]]},{"kind":"table","columns":["Claim","Significato"],"rows":[["`sub`","Il numero della scheda come stringa (`\"7\"`). Resta lo stesso per tutta la vita della scheda, e un alias non lo cambia mai."],["`name`","Il nome sulla scheda. In Veil i nomi non sono unici, quindi mostralo con il numero e riconosci le persone da `sub`. Mentre lo staff nasconde un nome, vale `Card <n>`."],["`picture`","L'indirizzo dell'immagine della scheda. Manca (o è `null` in UserInfo) se la scheda non ne ha."],["`preferred_username`","L'alias della scheda se ne ha uno, altrimenti `u<n>`. Un alias può cambiare o essere lasciato."],["`profile`","`https://veil.rip/u/<n>`, la pagina della scheda."],["`veil_version`","La versione della scheda con cui è stato controllato l'accesso. In UserInfo, la sua versione corrente."],["`veil_log`","La voce del registro di quella versione e il checkpoint in cui il server l'ha verificata: `{origin, index, size, root}`, dove `root` è l'hash della radice del checkpoint in base64."],["`veil_good_standing`","`true` quando non vale nessuno dei motivi qui sotto."],["`veil_standing_reasons`","Il perché: `suspended` (dallo staff di Veil), `closing` (la scheda ha chiesto di essere chiusa e ha 24 ore per ripensarci), `takeover` (durante l'attesa di 7 giorni, quando un'altra scheda chiede uno dei suoi account o lei ne chiede uno), `name_hidden`, `key_revoked` (la chiave di dispositivo che ha firmato questo accesso è stata revocata dopo), `key_banned`."],["`veil_consent`","Solo nel token ID. Il consenso esattamente come l'ha firmato la chiave di dispositivo, `{text, signature, key, alg}`. Vedi \"Cosa verificare\"."],["`veil_accounts`","Gli account verificati che la scheda ha adesso: `[{platform, id, handle, url, verified_at}]`. `id` è l'id stabile della piattaforma (gli id utente di Discord e GitHub, un indirizzo email, un dominio, un indirizzo del Fediverso), e `url` è `null` dove la piattaforma non ha una pagina di profilo. Un account in sospeso (durante un subentro) non c'è."],["`veil_orgs`","Le organizzazioni di cui la persona fa parte, accettate da tutte e due le parti: `[{n, name, role}]`."],["`veil_keys`","`{devices: [{key, alg}], external: [{type, fp}]}`: le chiavi di dispositivo attive, e le impronte delle chiavi pubbliche di altri sistemi (SSH, OpenPGP, minisign…) che la scheda certifica."],["`amr`","`[\"veil-key\"]`: l'accesso è stato la firma di una chiave di dispositivo."],["`auth_time`","Quando è stata controllata quella firma."],["`sid`","L'id dell'accesso, `l<numero>`."]]},{"kind":"paragraph","text":"Un token ID di un server di prova, con gli indirizzi scritti come sono su veil.rip:"},{"kind":"code","language":"json","code":"{\"iss\": \"https://veil.rip\", \"sub\": \"1\", \"aud\": \"veil_app_62bab7367bb583e2157490ae2cb65d4d\", \"azp\": \"veil_app_62bab7367bb583e2157490ae2cb65d4d\",\n \"name\": \"Elena Ferraro\", \"preferred_username\": \"u1\", \"profile\": \"https://veil.rip/u/1\",\n \"veil_version\": 1, \"veil_log\": {\"origin\": \"log.veil.rip/staging-1\", \"index\": 0, \"size\": 10, \"root\": \"JkW148VFq8g8MXHJArRQ/38q3ix5BrhSjAmHSAW8+P4=\"},\n \"veil_good_standing\": true, \"veil_standing_reasons\": [],\n \"iat\": 1793525884, \"exp\": 1793526484, \"auth_time\": 1793525884, \"nonce\": \"ozFe35gs4K2fjVX7jcR81jTMf5X8CNR6X12cZSMuIi0\",\n \"amr\": [\"veil-key\"], \"sid\": \"l9\",\n \"veil_consent\": {\"text\": \"veil-rubrica:oidc-consent:v1\\n{\\\"card\\\":1,\\\"challenge\\\":\\\"OpmVnrcB…\\\",…,\\\"v\\\":1}\",\n                  \"signature\": \"ac8e0fb6…026a06\", \"key\": \"120a568ad456a6dfdc16d48d1ab91af826180545a13123a49134968446f57195\", \"alg\": \"ed25519\"}}"},{"kind":"heading","text":"Token"},{"kind":"table","columns":["Token","Forma","Durata"],"rows":[["Codice","Opaco, si usa una volta","10 minuti. Usato due volte, chiude ogni token del suo accesso."],["Token di accesso","Opaco, `veil_at_…`, un token Bearer per UserInfo","10 minuti"],["Refresh token","Opaco, `veil_rt_…`, legato alla tua app","30 giorni, uno nuovo a ogni uso, e al più 90 giorni dopo l'accesso"],["Token ID","JWT firmato da Veil, con `kid`","10 minuti"]]},{"kind":"paragraph","text":"Veil tiene ogni codice e ogni token come hash SHA-256. L'endpoint dei token accetta `application/x-www-form-urlencoded`, con ogni parametro una volta sola."},{"kind":"list","items":["`grant_type=authorization_code` vuole `code`, `redirect_uri` e `code_verifier`.","`grant_type=refresh_token` vuole `refresh_token`, e `scope` può restringere gli scope purché tenga `openid`. La risposta porta un refresh token nuovo, e il vecchio smette di funzionare. Un refresh token usato una seconda volta chiude tutto l'accesso, perché qualcuno l'ha copiato.","Un refresh fallisce con `invalid_grant` quando la scheda è chiusa o sospesa, quando la chiave di dispositivo che ha firmato l'accesso è revocata dalla scheda, e quando la persona revoca la tua app dalla pagina del suo account.","`client_secret_basic` è HTTP Basic con il client id e il segreto. Per `private_key_jwt`, l'asserzione del client ha `iss` e `sub` uguali al tuo client id, `aud` uguale a `https://veil.rip` o all'indirizzo dell'endpoint dei token, `exp` al più 5 minuti avanti e un `jti` da 8 a 256 caratteri che vale una volta. Il tipo della tua chiave ne fissa l'algoritmo. Un'app pubblica manda `client_id`."]},{"kind":"paragraph","text":"Il token ID è firmato con `EdDSA`, a meno che la tua app non abbia registrato `ES256`. Molte librerie si aspettano `RS256` se non si configura niente, quindi imposta `id_token_signed_response_alg` nella configurazione del tuo client con l'algoritmo che hai registrato."},{"kind":"paragraph","text":"Il JWKS ha tutte e due le chiavi. Ogni chiave ha un solo `alg`, e il suo `kid` è la sua impronta RFC 7638. Le chiavi ruotano con 30 giorni di sovrapposizione, quindi una chiave sostituita resta nel JWKS per 30 giorni. Tieni il JWKS in cache per qualche minuto, e scaricalo di nuovo quando un token nomina un `kid` che non hai."},{"kind":"heading","text":"UserInfo","level":3},{"kind":"paragraph","text":"`GET` o `POST /oidc/userinfo` con `Authorization: Bearer <token di accesso>` restituisce i claim degli scope del token dallo stato della scheda adesso: la sua versione corrente, l'ultimo checkpoint verificato del registro, se è in regola, e i suoi account, le organizzazioni e le chiavi. Una chiave revocata o una sospensione si vedono subito."},{"kind":"paragraph","text":"Per una scheda sospesa la risposta è `401` con `WWW-Authenticate: Bearer error=\"veil_suspended\"` e `{\"error\": \"veil_suspended\"}`. I token di una scheda chiusa rispondono `invalid_token`, perché chiudere una scheda revoca ogni token che ha dato."},{"kind":"heading","text":"Revoca e introspezione","level":3},{"kind":"paragraph","text":"`POST /oidc/revoke` (RFC 7009) accetta `token`, con l'autenticazione dell'endpoint dei token. Revocare un refresh token chiude ogni token del suo accesso. Un token che non è della tua app, o che non è affatto un token, riceve comunque `200`."},{"kind":"paragraph","text":"`POST /oidc/introspect` (RFC 7662) permette a un'app confidenziale di chiedere dei propri token. La risposta è `{active, scope, client_id, sub, aud, iss, iat, exp, token_type, auth_time, veil_good_standing}`, oppure `{\"active\": false}`."},{"kind":"heading","text":"Logout","level":3},{"kind":"paragraph","text":"Veil non tiene una sessione da chiudere, perché un accesso è una firma. `GET /oidc/logout?id_token_hint=<un token ID>&post_logout_redirect_uri=…&state=…` revoca i token di quell'accesso (il suo `sid`) e rimanda il browser all'URI di logout se la tua app ha registrato esattamente quell'indirizzo. Altrimenti una pagina dice alla persona che è uscita dalla tua app."},{"kind":"heading","text":"Errori","level":3,"anchor":"token-errors"},{"kind":"paragraph","text":"Gli endpoint OAuth rispondono agli errori nella forma del loro protocollo, `{\"error\": \"invalid_grant\", \"error_description\": \"…\"}`, con `Cache-Control: no-store`. Su quegli endpoint anche i limiti tengono quella forma: `429` con `Retry-After` e `{\"error\": \"rate_limited\", \"error_description\": \"…\"}`."},{"kind":"paragraph","text":"I limiti sono 1200 chiamate al minuto per IP sul canale tra server (token, revoca, introspezione, UserInfo), 600 al minuto per app dopo l'autenticazione, 60 richieste di autorizzazione al minuto per IP e 10 registrazioni l'ora per IP. Il JSON della pagina di consenso e le API di gestione usano la busta della rubrica, `{\"error\": {\"code\", \"message\"}}`."},{"kind":"heading","text":"Cosa verificare"},{"kind":"heading","text":"Ogni app","level":3},{"kind":"paragraph","text":"Sono i controlli standard di OpenID Connect. Una libreria mantenuta li fa per te, anche se in molte il passo 3 va acceso."},{"kind":"checks","items":[{"text":"`state` e `iss` nella risposta di autorizzazione sono il tuo state e `https://veil.rip`.","why":"`state` lega la risposta al browser che ha cominciato l'accesso, così un altro sito non può infilare un suo codice nel tuo callback. `iss` dice quale provider ha risposto, così a un'app che parla con più provider non si può passare il codice di uno come se fosse di un altro (RFC 9207)."},{"text":"Il codice torna indietro con il tuo verificatore PKCE.","why":"Solo il tuo server conosce il verificatore dietro il `code_challenge` che ha mandato, quindi un codice uscito per strada (un log, un referer, un'altra app sullo stesso dispositivo) non serve a chi lo raccoglie."},{"text":"La firma del token ID si verifica con la chiave del JWKS che il suo `kid` nomina, con l'algoritmo di quella chiave. Non lasciare mai che sia l'intestazione a scegliere l'algoritmo, e non accettare mai `none` o un algoritmo HMAC. Molte librerie saltano questo controllo per un token ID che arriva diretto dall'endpoint dei token, perché lì OpenID Connect lascia che TLS ne faccia le veci. openid-client lo salta se non chiami `enableNonRepudiationChecks`. Accendi il controllo.","why":"Con il controllo acceso, il token che tieni prova quello che Veil ha detto su chi è entrato. Prendere l'algoritmo dalla chiave, e mai dall'intestazione, ferma un token falso che dice `none` o usa la chiave pubblica come segreto HMAC."},{"text":"`iss` è `https://veil.rip`, `aud` (e `azp`) è il tuo client id, `exp` è nel futuro, `iat` no, e `nonce` è il tuo.","why":"Una firma valida dice solo che il token l'ha fatto Veil. Questi claim dicono che l'ha fatto per la tua app, per questo accesso e da poco, quindi un token dato a un'altra app o ripreso da un accesso di prima qui non passa."},{"text":"Riconosci la persona da `sub`. Mostra `name` con il numero o l'indirizzo della scheda.","why":"`sub` è il numero della scheda e resta lo stesso per tutta la sua vita. In Veil i nomi non sono unici, e un alias può cambiare o essere lasciato, quindi nessuno dei due può stare al posto della persona."},{"text":"Se alla tua app importa che la scheda sia in regola, leggi `veil_good_standing`, e chiedi di nuovo a UserInfo prima di un'azione importante.","why":"Il token ID descrive la scheda al momento dell'accesso. UserInfo riflette la scheda adesso, quindi una sospensione, un subentro o una chiave revocata lì si vedono subito."}]},{"kind":"heading","text":"Il vantaggio di Veil: l'accesso controllato sul registro","level":3,"anchor":"log-check"},{"kind":"paragraph","text":"I controlli qui sopra si fidano del server per sapere chi è entrato. Con lo scope `veil` puoi controllare invece la firma della persona e il registro pubblico."},{"kind":"checks","items":[{"text":"Leggi `veil_consent.text`. La prima riga è `veil-rubrica:oidc-consent:v1`, e il resto è JSON canonico con `v`, `iss`, `card`, `key`, `client`, `redirect`, `scopes`, `nonce`, `state`, `challenge`, `request` e `time`.","why":"La prima riga dice che tipo di testo ha firmato la chiave, così una firma che la chiave ha fatto per qualunque altra cosa in Veil non può passare per il consenso di un accesso."},{"text":"Verifica `veil_consent.signature` (Ed25519 in esadecimale) sui byte esatti di `text` con `veil_consent.key`.","why":"Questa firma viene dalla chiave di dispositivo della persona. Il server non ha una copia di quella chiave e non può farla."},{"text":"Controlla che `challenge` sia lo S256 del tuo verificatore PKCE, `nonce` il tuo nonce, `state` lo SHA-256 (in esadecimale) del tuo state, `client` il tuo client id, `iss` l'issuer, `key` uguale a `veil_consent.key` e `card` uguale a `sub`. `veil_consent.alg` è `ed25519`.","why":"Il tuo server ha fatto il verificatore solo per questo accesso, quindi un consenso che nomina il suo `code_challenge` è stato firmato per questo accesso e per nessun altro."},{"text":"Chiedi al server dove guardare. `GET /v1/keys/{key}` dà la scheda e la versione che ha aggiunto la chiave (`addedIn`). `GET /v1/identities/{sub}` dà il `token` della scheda, con cui il registro la nomina, e la sua `version` corrente. `veil_version` deve stare tra le due.","why":"In questo passo niente è ancora una prova. Ti dice quali voci del registro leggere nel passo dopo."},{"text":"Controllalo nel registro, per ogni versione da `addedIn` alla corrente. `GET /v1/identities/{sub}/versions/{v}/proof` dà la voce del registro, il suo indice, la prova di inclusione e un checkpoint. Il `label` della voce deve nominare la scheda (`identity` è il token), la versione (`n`) e quella prima (`prev` è il `labelHash` della prova precedente). L'hash della foglia è `SHA-256(0x00 || entry)`, e la prova di inclusione (RFC 6962) deve portare alla radice del checkpoint. La prima di quelle versioni deve aggiungere la chiave come dispositivo (`label.auth` contiene `{\"op\": \"addKey\", \"key\": <key>, \"role\": \"device\"}`), nessuna dopo può revocarla (`revokeKey`), e la prova di `veil_version` deve stare a `veil_log.index`. Di rado una scheda ha molte versioni dopo l'arrivo di una chiave, e puoi tenere quelle già controllate per la volta dopo.","why":"Fin qui lo diceva solo il server. Adesso lo dice il registro pubblico: la chiave è stata aggiunta a questa scheda come dispositivo, nessuna versione dopo l'ha revocata, e la versione dell'accesso è la voce che Veil ha nominato. Un dispositivo revocato, la voce di un'altra scheda spacciata per questa o una versione spostata qui non passano."},{"text":"Controlla ogni checkpoint con una copia della policy del registro che tieni con la tua app (`https://log.veil.rip/<log>/policy`, il registro di `veil_log.origin`): la firma del registro, le controfirme di abbastanza testimoni della policy da fare il suo quorum, e una dimensione almeno pari a `veil_log.size`.","why":"I testimoni sono operatori indipendenti, quindi un server che ti mostrasse una versione della scheda che nessun altro vede verrebbe scoperto."}]},{"kind":"paragraph","text":"La prova di conformità di Veil fa girare gli esempi Node e Python qui sotto contro un server che mente. Rifiutano un consenso firmato da un dispositivo che la scheda ha revocato, la voce del registro di un'altra scheda spacciata per questa, una chiave revocata che il server dice attiva e una versione dell'accesso spostata."},{"kind":"heading","text":"Limiti dichiarati","level":3},{"kind":"list","style":"limits","items":["Il registro conosce la scheda per il token, non per il numero. Il passaggio da `sub` al token resta la parola del server.","Il passo 5 non controlla la firma di ogni voce. Il registro prende una voce solo se l'ha firmata una chiave con autorità sulla scheda, e chiunque può ricontrollare quelle firme rifacendo l'autorità della scheda dalle versioni nel registro (`/v1/identities/{n}/authority`), come fanno le pagine delle schede nel browser con `veil-v2.js`. Quello è il controllo del tutto indipendente.","Un server che nasconde le versioni più nuove della scheda, e dice che la scheda è ancora a una più vecchia, non viene scoperto da questi passi. È il punto dell'indice del formato 2 del registro.","Gli account, le organizzazioni e le chiavi nei claim sono la lettura che il server fa della scheda. Ognuno sta anche nello stato firmato della scheda, che la pagina di una scheda controlla nel browser di chi la guarda.","Un'app che controlla solo la parte OpenID Connect si fida del server per l'accesso stesso, come con qualunque provider OpenID Connect. Un server in mano ad altri potrebbe dare a un'app così un token per qualunque scheda. Un'app che controlla `veil_consent` con il proprio verificatore PKCE non si inganna così senza la chiave di dispositivo della persona. La chiave non lascia mai il browser, ma le pagine che la usano arrivano dallo stesso server. Un server in mano ad altri può mandare una pagina che firma il consenso per l'accesso di un attaccante la prossima volta che la persona apre Veil, anche dentro la cornice nascosta della chat, che si carica a ogni visita. È il limite di ogni chiave tenuta da una pagina web.","La persona vede ogni accesso sulla pagina del suo account, con il testo firmato e la firma. La pagina li mostra e non li controlla. Le ricevute firmate degli accessi nel registro pubblico, con cui una persona potrebbe accorgersi di un token dato senza di lei anche a un'app che non controlla, sono un punto aperto per dopo.","Un server può ancora rifiutare di rispondere, o servire un checkpoint più vecchio. Se ti importa che sia recente, confronta l'ora delle controfirme dei testimoni con l'orologio."]},{"kind":"heading","text":"Esempio: Node","anchor":"example-node"},{"kind":"paragraph","text":"Il flusso usa `openid-client` (fissato alla 6.8.8 nella prova di conformità di Veil), e i controlli di Veil usano `node:crypto`. Il verificatore PKCE, lo state e il nonce restano sul tuo server, legati a un cookie. Serve Node 20 o più recente."},{"kind":"code","language":"javascript","code":"// Sign in with Veil in a Node server: openid-client for the OpenID Connect flow, node:crypto for the Veil checks.\n// npm install openid-client@6.8.8      node veil-signin.mjs\nimport * as oidc from \"openid-client\";\nimport { createHash, createPublicKey, verify, randomBytes } from \"node:crypto\";\nimport { createServer } from \"node:http\";\n\nconst ISSUER = new URL(process.env.VEIL_ISSUER ?? \"https://veil.rip\");\nconst CLIENT_ID = process.env.VEIL_CLIENT_ID;          // from /developers\nconst CLIENT_SECRET = process.env.VEIL_CLIENT_SECRET;  // shown once, keep it in your secret store\nconst REDIRECT = process.env.VEIL_REDIRECT ?? \"http://localhost:3000/callback\";\n// A copy of the log's policy, kept with your app: the log's key and its witnesses (https://log.veil.rip/<log>/policy).\nconst POLICY = process.env.VEIL_POLICY_TEXT;\n\nconst sha256 = (b) => createHash(\"sha256\").update(b).digest();\nconst ed25519 = (hex) => createPublicKey({ key: { kty: \"OKP\", crv: \"Ed25519\", x: Buffer.from(hex, \"hex\").toString(\"base64url\") }, format: \"jwk\" });\n\n// RFC 6962: the root an inclusion proof leads to, or null.\nfunction rootFrom(leaf, index, size, path) {\n  if (index >= size) return null;\n  let fn = BigInt(index), sn = BigInt(size) - 1n, r = leaf;\n  for (const p of path) {\n    if (sn === 0n) return null;\n    if ((fn & 1n) === 1n || fn === sn) {\n      r = sha256(Buffer.concat([Buffer.from([1]), p, r]));\n      if ((fn & 1n) === 0n) while ((fn & 1n) === 0n && fn !== 0n) { fn >>= 1n; sn >>= 1n; }\n    } else r = sha256(Buffer.concat([Buffer.from([1]), r, p]));\n    fn >>= 1n; sn >>= 1n;\n  }\n  return sn === 0n ? r : null;\n}\n\n// A C2SP note key \"name+hash+base64(type || key)\": type 0x01 for the log, 0x04 for a witness.\nfunction noteKey(vkey) {\n  const i = vkey.indexOf(\"+\"), j = vkey.indexOf(\"+\", i + 1);\n  const name = vkey.slice(0, i), raw = Buffer.from(vkey.slice(j + 1), \"base64\");\n  const id = sha256(Buffer.concat([Buffer.from(name + \"\\n\"), raw])).subarray(0, 4);\n  if (id.toString(\"hex\") !== vkey.slice(i + 1, j)) throw new Error(`bad vkey ${name}`);\n  return { name, id, key: ed25519(raw.subarray(1).toString(\"hex\")) };\n}\n\n// The checkpoint's log signature, and how many witnesses of the policy's quorum group cosigned it.\nfunction checkCheckpoint(note, policyText) {\n  const text = note.slice(0, note.indexOf(\"\\n\\n\") + 1);\n  const sigs = note.slice(text.length + 1).split(\"\\n\").filter((l) => l.startsWith(\"— \")).map((l) => l.slice(2).split(\" \"));\n  const lines = policyText.split(\"\\n\").map((l) => l.trim().split(/\\s+/)).filter((f) => f[0] && !f[0].startsWith(\"#\"));\n  const log = noteKey(lines.find((f) => f[0] === \"log\")[1]);\n  const witnesses = new Map(lines.filter((f) => f[0] === \"witness\").map((f) => [f[1], noteKey(f[2])]));\n  const group = lines.find((f) => f[0] === \"group\" && f[1] === lines.find((q) => q[0] === \"quorum\")[1]);\n  const members = group.slice(3);\n  const needed = group[2] === \"all\" ? members.length : group[2] === \"any\" ? 1 : Number(group[2]);\n  let logSigned = false; const cosigned = new Set();\n  for (const [name, b64] of sigs) {\n    const blob = Buffer.from(b64, \"base64\");\n    if (name === log.name && blob.subarray(0, 4).equals(log.id)) logSigned ||= verify(null, Buffer.from(text), log.key, blob.subarray(4));\n    for (const [id, w] of witnesses) {\n      if (name !== w.name || blob.length !== 76 || !blob.subarray(0, 4).equals(w.id)) continue;\n      const msg = `cosignature/v1\\ntime ${blob.readBigUInt64BE(4)}\\n${text}`;\n      if (verify(null, Buffer.from(msg), w.key, blob.subarray(12))) cosigned.add(id);\n    }\n  }\n  const [origin, size, root] = text.split(\"\\n\");\n  return { logSigned, quorum: members.filter((m) => cosigned.has(m)).length >= needed, origin, size: Number(size), root };\n}\n\n/**\n * Veil's own checks on the claims of a verified ID token (scope \"veil\"): the person's device key signed this very login\n * (your PKCE challenge, your nonce, your client id), and the public log, not the server, says the key is on the card: the\n * version that added it, every version after it up to the card's current one (none revokes it), and the version the login\n * was checked against, each one this card's entry, in one chain, under a checkpoint the log signed and a quorum of witnesses\n * cosigned.\n */\nexport async function verifyVeil(claims, { verifier, nonce, state, clientId, issuer, policyText }) {\n  const c = claims.veil_consent;\n  if (!c || c.alg !== \"ed25519\" || !c.text.startsWith(\"veil-rubrica:oidc-consent:v1\\n\")) throw new Error(\"no signed consent\");\n  if (!verify(null, Buffer.from(c.text, \"utf8\"), ed25519(c.key), Buffer.from(c.signature, \"hex\"))) throw new Error(\"consent signature\");\n  const s = JSON.parse(c.text.slice(c.text.indexOf(\"\\n\") + 1));\n  if (s.challenge !== sha256(verifier).toString(\"base64url\")) throw new Error(\"not your PKCE challenge\");\n  if (s.client !== clientId || s.nonce !== nonce || s.iss !== issuer || s.key !== c.key || String(s.card) !== claims.sub) throw new Error(\"not your login\");\n  if (s.state !== sha256(state).toString(\"hex\")) throw new Error(\"not your state\");\n  const get = async (path) => { const r = await fetch(issuer + path); if (!r.ok) throw new Error(`${path}: ${r.status}`); return r.json(); };\n  // The server says where to look: the version that added the key, the card's id in the log (its token), its current version.\n  const key = await get(`/v1/keys/${c.key}`);\n  const card = await get(`/v1/identities/${claims.sub}`);\n  if (String(key.identity?.n) !== claims.sub || key.role !== \"device\") throw new Error(\"key not on the card\");\n  if (!(key.addedIn <= claims.veil_version && claims.veil_version <= card.version)) throw new Error(\"the login's version is not one where the key was on the card\");\n  // The log says the rest.\n  let prev = null;\n  for (let v = key.addedIn; v <= card.version; v++) {\n    const proof = await get(`/v1/identities/${claims.sub}/versions/${v}/proof`);\n    const label = JSON.parse(JSON.parse(proof.entry).label);\n    if (label.identity !== card.token || label.n !== v || (prev !== null && label.prev !== prev)) throw new Error(`the log's entry is not version ${v} of this card`);\n    const leaf = sha256(Buffer.concat([Buffer.from([0]), Buffer.from(proof.entry, \"utf8\")]));\n    const root = rootFrom(leaf, proof.logIndex, proof.checkpoint.size, proof.inclusion.map((h) => Buffer.from(h, \"hex\")));\n    if (!root || root.toString(\"base64\") !== proof.checkpoint.root) throw new Error(\"inclusion proof\");\n    const cp = checkCheckpoint(proof.checkpoint.note, policyText);\n    if (!cp.logSigned || !cp.quorum || cp.origin !== claims.veil_log.origin || cp.root !== proof.checkpoint.root || cp.size !== proof.checkpoint.size) throw new Error(\"checkpoint\");\n    if (cp.size < claims.veil_log.size) throw new Error(\"an older checkpoint than the login's\");\n    const auth = label.auth ?? [];\n    if (v === key.addedIn && !auth.some((op) => op.op === \"addKey\" && op.key === c.key && op.role === \"device\")) throw new Error(\"the log does not add this key here\");\n    if (v > key.addedIn && auth.some((op) => op.op === \"revokeKey\" && op.key === c.key)) throw new Error(\"the log revokes this key\");\n    if (v === claims.veil_version && proof.logIndex !== claims.veil_log.index) throw new Error(\"another log entry\");\n    prev = proof.labelHash;\n  }\n  return { card: Number(claims.sub), version: claims.veil_version, goodStanding: claims.veil_good_standing };\n}\n\n// The flow, with the PKCE verifier, state and nonce kept server-side per browser (a cookie names the session).\nif (import.meta.url === `file://${process.argv[1]}`) {\n  // enableNonRepudiationChecks: without it openid-client does not check the ID token's signature (TLS stands in for it).\n  const config = await oidc.discovery(ISSUER, CLIENT_ID, { id_token_signed_response_alg: \"EdDSA\" }, oidc.ClientSecretBasic(CLIENT_SECRET),\n    { execute: [oidc.enableNonRepudiationChecks] });\n  const sessions = new Map();\n  createServer(async (req, res) => {\n    const url = new URL(req.url, REDIRECT);\n    const sid = /sid=([A-Za-z0-9_-]+)/.exec(req.headers.cookie ?? \"\")?.[1];\n    if (url.pathname === \"/login\") {\n      const s = { verifier: oidc.randomPKCECodeVerifier(), state: oidc.randomState(), nonce: oidc.randomNonce() };\n      const id = randomBytes(16).toString(\"base64url\");\n      sessions.set(id, s);\n      const to = oidc.buildAuthorizationUrl(config, { redirect_uri: REDIRECT, scope: \"openid profile veil veil:accounts\", state: s.state, nonce: s.nonce,\n        code_challenge: await oidc.calculatePKCECodeChallenge(s.verifier), code_challenge_method: \"S256\" });\n      res.writeHead(302, { Location: to.href, \"Set-Cookie\": `sid=${id}; HttpOnly; Secure; SameSite=Lax; Path=/` }).end();\n    } else if (url.pathname === \"/callback\" && sessions.has(sid)) {\n      const s = sessions.get(sid); sessions.delete(sid);\n      try {\n        // state, iss (RFC 9207), PKCE, the ID token's signature (with enableNonRepudiationChecks), iss, aud, exp and nonce.\n        const tokens = await oidc.authorizationCodeGrant(config, url, { pkceCodeVerifier: s.verifier, expectedState: s.state, expectedNonce: s.nonce });\n        const claims = tokens.claims();\n        const veil = await verifyVeil(claims, { ...s, clientId: CLIENT_ID, issuer: config.serverMetadata().issuer, policyText: POLICY });\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" }).end(`Signed in as card ${veil.card} (${claims.name}), checked against the log\\n`);\n      } catch (e) { res.writeHead(400, { \"Content-Type\": \"text/plain\" }).end(`Sign-in refused: ${e.message}\\n`); }\n    } else res.writeHead(404).end();\n  }).listen(3000);\n}"},{"kind":"heading","text":"Esempio: Python","anchor":"example-python"},{"kind":"paragraph","text":"La libreria standard di Python non ha Ed25519 né ECDSA, quindi questo esempio usa `cryptography` (pyca), una libreria presente in quasi ogni distribuzione che li ha tutti e due. Il resto viene dalla libreria standard. Fai girare il flusso nel tuo framework web: `start_login` dà il redirect e i valori da tenere nella sessione, `finish_login` gestisce il callback, e `verify_veil` aggiunge i controlli sul registro."},{"kind":"code","language":"python","code":"\"\"\"Sign in with Veil from Python: the standard library for HTTP, JSON and hashes, and pyca/cryptography for the two\nsignature schemes (Ed25519, ECDSA P-256), which the standard library does not have.   pip install cryptography\"\"\"\nimport base64, hashlib, json, secrets, time, urllib.parse, urllib.request\nfrom cryptography.exceptions import InvalidSignature\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey\nfrom cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature\n\nISSUER = \"https://veil.rip\"\n\n\ndef b64url(data: str) -> bytes:\n    return base64.urlsafe_b64decode(data + \"=\" * (-len(data) % 4))\n\n\ndef get_json(url: str, data: bytes | None = None, headers: dict | None = None):\n    req = urllib.request.Request(url, data=data, headers={\"Accept\": \"application/json\", **(headers or {})})\n    with urllib.request.urlopen(req, timeout=15) as r:\n        return json.load(r)\n\n\ndef ed25519_ok(key: bytes, sig: bytes, msg: bytes) -> bool:\n    try:\n        Ed25519PublicKey.from_public_bytes(key).verify(sig, msg)\n        return True\n    except (InvalidSignature, ValueError):\n        return False\n\n\n# ---------- the flow ----------\n\ndef start_login(discovery: dict, client_id: str, redirect_uri: str) -> tuple[str, dict]:\n    \"\"\"The authorization URL, and what to keep server-side for the callback (never in the URL or a readable cookie).\"\"\"\n    session = {\"verifier\": secrets.token_urlsafe(48), \"state\": secrets.token_urlsafe(24), \"nonce\": secrets.token_urlsafe(24)}\n    challenge = base64.urlsafe_b64encode(hashlib.sha256(session[\"verifier\"].encode()).digest()).rstrip(b\"=\").decode()\n    query = urllib.parse.urlencode({\"response_type\": \"code\", \"client_id\": client_id, \"redirect_uri\": redirect_uri,\n                                    \"scope\": \"openid profile veil veil:accounts\", \"state\": session[\"state\"], \"nonce\": session[\"nonce\"],\n                                    \"code_challenge\": challenge, \"code_challenge_method\": \"S256\"})\n    return f\"{discovery['authorization_endpoint']}?{query}\", session\n\n\ndef finish_login(discovery: dict, client_id: str, secret: str, redirect_uri: str, callback_query: str, session: dict) -> dict:\n    \"\"\"The callback: state and iss first (RFC 9207), then the code for tokens, then the ID token's checks.\"\"\"\n    q = dict(urllib.parse.parse_qsl(callback_query))\n    if q.get(\"state\") != session[\"state\"] or q.get(\"iss\") != discovery[\"issuer\"]:\n        raise ValueError(\"state or iss is not this sign-in's\")\n    if \"error\" in q:\n        raise ValueError(f\"the sign-in did not happen: {q['error']}\")\n    form = urllib.parse.urlencode({\"grant_type\": \"authorization_code\", \"code\": q[\"code\"], \"redirect_uri\": redirect_uri,\n                                   \"code_verifier\": session[\"verifier\"]}).encode()\n    basic = base64.b64encode(f\"{urllib.parse.quote(client_id, safe='')}:{urllib.parse.quote(secret, safe='')}\".encode()).decode()\n    tokens = get_json(discovery[\"token_endpoint\"], form, {\"Authorization\": f\"Basic {basic}\", \"Content-Type\": \"application/x-www-form-urlencoded\"})\n    return verify_id_token(discovery, tokens[\"id_token\"], client_id, session[\"nonce\"])\n\n\ndef verify_id_token(discovery: dict, token: str, client_id: str, nonce: str) -> dict:\n    \"\"\"The JWS checked with the key its kid names, with that key's one algorithm (never the header's choice), then the claims.\"\"\"\n    head_b64, body_b64, sig_b64 = token.split(\".\")\n    header, claims, sig = json.loads(b64url(head_b64)), json.loads(b64url(body_b64)), b64url(sig_b64)\n    jwk = next(k for k in get_json(discovery[\"jwks_uri\"])[\"keys\"] if k[\"kid\"] == header[\"kid\"])\n    signed = f\"{head_b64}.{body_b64}\".encode()\n    if header[\"alg\"] != jwk[\"alg\"]:\n        raise ValueError(\"the token's alg is not its key's\")\n    if jwk[\"alg\"] == \"EdDSA\":\n        ok = ed25519_ok(b64url(jwk[\"x\"]), sig, signed)\n    elif jwk[\"alg\"] == \"ES256\":\n        key = ec.EllipticCurvePublicNumbers(int.from_bytes(b64url(jwk[\"x\"])), int.from_bytes(b64url(jwk[\"y\"])), ec.SECP256R1()).public_key()\n        try:\n            key.verify(encode_dss_signature(int.from_bytes(sig[:32]), int.from_bytes(sig[32:])), signed, ec.ECDSA(hashes.SHA256()))\n            ok = True\n        except InvalidSignature:\n            ok = False\n    else:\n        ok = False\n    now = time.time()\n    if not ok or claims[\"iss\"] != discovery[\"issuer\"] or claims[\"aud\"] != client_id or claims.get(\"azp\", client_id) != client_id:\n        raise ValueError(\"ID token signature, issuer or audience\")\n    if claims[\"exp\"] < now - 30 or claims[\"iat\"] > now + 30 or claims.get(\"nonce\") != nonce:\n        raise ValueError(\"ID token expired, from the future, or not this sign-in's nonce\")\n    return claims\n\n\n# ---------- Veil's own checks ----------\n\ndef root_from(leaf: bytes, index: int, size: int, path: list[bytes]) -> bytes | None:\n    \"\"\"RFC 6962: the root an inclusion proof leads to.\"\"\"\n    if index >= size:\n        return None\n    fn, sn, r = index, size - 1, leaf\n    for p in path:\n        if sn == 0:\n            return None\n        if fn & 1 or fn == sn:\n            r = hashlib.sha256(b\"\\x01\" + p + r).digest()\n            while not fn & 1 and fn != 0:\n                fn, sn = fn >> 1, sn >> 1\n        else:\n            r = hashlib.sha256(b\"\\x01\" + r + p).digest()\n        fn, sn = fn >> 1, sn >> 1\n    return r if sn == 0 else None\n\n\ndef note_key(vkey: str) -> tuple[str, bytes, bytes]:\n    name, digest, b64 = vkey.split(\"+\", 2)\n    raw = base64.b64decode(b64)\n    key_id = hashlib.sha256(name.encode() + b\"\\n\" + raw).digest()[:4]\n    if key_id.hex() != digest:\n        raise ValueError(f\"bad vkey {name}\")\n    return name, key_id, raw[1:]\n\n\ndef check_checkpoint(note: str, policy_text: str) -> dict:\n    \"\"\"The log's signature on the checkpoint, and whether the policy's quorum of witnesses cosigned it.\"\"\"\n    text = note[: note.index(\"\\n\\n\") + 1]\n    sigs = [line[2:].split(\" \") for line in note[len(text) + 1:].split(\"\\n\") if line.startswith(\"— \")]\n    lines = [line.split() for line in policy_text.splitlines() if line.strip() and not line.startswith(\"#\")]\n    log_name, log_id, log_key = note_key(next(f for f in lines if f[0] == \"log\")[1])\n    witnesses = {f[1]: note_key(f[2]) for f in lines if f[0] == \"witness\"}\n    quorum_group = next(f for f in lines if f[0] == \"quorum\")[1]\n    group = next(f for f in lines if f[0] == \"group\" and f[1] == quorum_group)\n    members, rule = group[3:], group[2]\n    needed = len(members) if rule == \"all\" else 1 if rule == \"any\" else int(rule)\n    log_signed, cosigned = False, set()\n    for name, sig_b64 in sigs:\n        blob = base64.b64decode(sig_b64)\n        if name == log_name and blob[:4] == log_id and ed25519_ok(log_key, blob[4:], text.encode()):\n            log_signed = True\n        for wid, (wname, wkey_id, wkey) in witnesses.items():\n            if name == wname and len(blob) == 76 and blob[:4] == wkey_id:\n                msg = f\"cosignature/v1\\ntime {int.from_bytes(blob[4:12])}\\n{text}\".encode()\n                if ed25519_ok(wkey, blob[12:], msg):\n                    cosigned.add(wid)\n    origin, size, root = text.split(\"\\n\")[:3]\n    return {\"log_signed\": log_signed, \"quorum\": len(cosigned & set(members)) >= needed, \"origin\": origin, \"size\": int(size), \"root\": root}\n\n\ndef verify_veil(claims: dict, session: dict, client_id: str, issuer: str, policy_text: str) -> dict:\n    \"\"\"The person's device key signed this very login, and the public log, not the server, says the key is on the card: the\n    version that added it, every version after it up to the card's current one (none revokes it), the login's version.\"\"\"\n    c = claims[\"veil_consent\"]\n    if c.get(\"alg\") != \"ed25519\" or not c[\"text\"].startswith(\"veil-rubrica:oidc-consent:v1\\n\"):\n        raise ValueError(\"not a consent\")\n    if not ed25519_ok(bytes.fromhex(c[\"key\"]), bytes.fromhex(c[\"signature\"]), c[\"text\"].encode()):\n        raise ValueError(\"consent signature\")\n    s = json.loads(c[\"text\"].split(\"\\n\", 1)[1])\n    challenge = base64.urlsafe_b64encode(hashlib.sha256(session[\"verifier\"].encode()).digest()).rstrip(b\"=\").decode()\n    if s[\"challenge\"] != challenge or s[\"nonce\"] != session[\"nonce\"] or s[\"client\"] != client_id or s[\"iss\"] != issuer \\\n            or s[\"key\"] != c[\"key\"] or str(s[\"card\"]) != claims[\"sub\"]:\n        raise ValueError(\"this consent is not for this login\")\n    if s[\"state\"] != hashlib.sha256(session[\"state\"].encode()).hexdigest():\n        raise ValueError(\"not this login's state\")\n    # The server says where to look: the version that added the key, the card's id in the log (its token), its current version.\n    key = get_json(f\"{issuer}/v1/keys/{c['key']}\")\n    card = get_json(f\"{issuer}/v1/identities/{claims['sub']}\")\n    if str(key[\"identity\"][\"n\"]) != claims[\"sub\"] or key[\"role\"] != \"device\":\n        raise ValueError(\"the key is not on the card\")\n    if not key[\"addedIn\"] <= claims[\"veil_version\"] <= card[\"version\"]:\n        raise ValueError(\"the login's version is not one where the key was on the card\")\n    # The log says the rest.\n    prev = None\n    for v in range(key[\"addedIn\"], card[\"version\"] + 1):\n        proof = get_json(f\"{issuer}/v1/identities/{claims['sub']}/versions/{v}/proof\")\n        label = json.loads(json.loads(proof[\"entry\"])[\"label\"])\n        if label[\"identity\"] != card[\"token\"] or label[\"n\"] != v or (prev is not None and label[\"prev\"] != prev):\n            raise ValueError(f\"the log's entry is not version {v} of this card\")\n        leaf = hashlib.sha256(b\"\\x00\" + proof[\"entry\"].encode()).digest()\n        root = root_from(leaf, proof[\"logIndex\"], proof[\"checkpoint\"][\"size\"], [bytes.fromhex(h) for h in proof[\"inclusion\"]])\n        if root is None or base64.b64encode(root).decode() != proof[\"checkpoint\"][\"root\"]:\n            raise ValueError(\"inclusion proof\")\n        cp = check_checkpoint(proof[\"checkpoint\"][\"note\"], policy_text)\n        if not (cp[\"log_signed\"] and cp[\"quorum\"] and cp[\"origin\"] == claims[\"veil_log\"][\"origin\"] and cp[\"root\"] == proof[\"checkpoint\"][\"root\"]):\n            raise ValueError(\"checkpoint signatures\")\n        if cp[\"size\"] < claims[\"veil_log\"][\"size\"]:\n            raise ValueError(\"an older checkpoint than the login's\")\n        auth = label.get(\"auth\", [])\n        if v == key[\"addedIn\"] and not any(op.get(\"op\") == \"addKey\" and op.get(\"key\") == c[\"key\"] and op.get(\"role\") == \"device\" for op in auth):\n            raise ValueError(\"the log does not add this key here\")\n        if v > key[\"addedIn\"] and any(op.get(\"op\") == \"revokeKey\" and op.get(\"key\") == c[\"key\"] for op in auth):\n            raise ValueError(\"the log revokes this key\")\n        if v == claims[\"veil_version\"] and proof[\"logIndex\"] != claims[\"veil_log\"][\"index\"]:\n            raise ValueError(\"another log entry\")\n        prev = proof[\"labelHash\"]\n    return {\"card\": int(claims[\"sub\"]), \"version\": claims[\"veil_version\"], \"good_standing\": claims[\"veil_good_standing\"]}"},{"kind":"paragraph","text":"La prova di conformità di Veil (`server/browser-test/oidc-conformance.mjs`, `OidcConformanceTest`) prende tutti e due gli esempi da `docs/oidc.md` nel repository, che ha lo stesso codice di questa pagina, e li fa girare contro un server Veil con un registro e testimoni veri, su accessi onesti e su accessi manomessi."},{"kind":"heading","text":"Riferimento"},{"kind":"paragraph","text":"Ogni endpoint ha la sua pagina, con i parametri, le risposte e gli errori."},{"kind":"links","ids":["oidc-discovery","oidc-jwks","oidc-authorize","oidc-token","oidc-userinfo","oidc-revoke","oidc-introspect","oidc-register","oidc-logout","oidc-clients","oidc-client-create","oidc-apps"]}],"related":["api-keys","signed-requests","proofs"],"errors":[],"documentationUrl":"https://veil.rip/docs/sign-in?lang=it"}}