Implémentation du tiers de confiance

Pour implémenter la validation des adresses e-mail sur votre site, mettez à jour le balisage de votre formulaire afin de demander le jeton et ajoutez la validation côté serveur pour les jetons entrants.

S'inscrire à la version d'évaluation

Les sites à valider doivent avoir configuré l'Origin Trial.

À partir de Chrome 154, les versions d'essai d'origine tierce sont compatibles avec une mise en garde importante : l'origine enregistrée pour la version d'essai doit être sur le même site que l'émetteur. Exemple :

  • Domaine de l'émetteur : issuer.example
  • Demandeur d'enregistrement OT : https://issuer.example
  • Origine JavaScript : https://issuer.example (ou https://app.issuer.example avec la correspondance de sous-domaine)

Configurer les champs du formulaire

Ajoutez un champ de jeton masqué à votre formulaire d'envoi d'e-mails :

<input
  type="email"
  name="email-address"
  autocomplete="email">
<input
  type="hidden"
  name="token"
  autocomplete="email-verification-token"
  nonce="rAnD0m-VaLuE">

Exigences concernant les champs :

  • Champ "Adresse e-mail" : définissez type="email" et autocomplete="email" pour que Chrome puisse remplir automatiquement l'adresse et la reconnaître.
  • Attributs du champ de jeton :
    • Définissez autocomplete="email-verification-token" : Chrome identifie ce champ pour renseigner le jeton lors de l'envoi.
    • Définissez nonce="<VALUE>" : le site doit fournir un nonce unique lié à la session pour valider l'envoi du formulaire.

Valider le jeton de validation de l'adresse e-mail (EVT)

Lorsque l'utilisateur envoie le formulaire, votre serveur reçoit l'adresse e-mail et le jeton du champ masqué. Un champ de jeton vide indique que le navigateur ou le fournisseur ne sont pas compatibles avec la validation étendue, ou que l'utilisateur a ignoré la validation. Si cela se produit, revenez à votre procédure de validation existante, par exemple en envoyant un code secret à usage unique ou un lien magique.

Si un jeton est présent, validez-le comme suit :

  1. Analysez le jeton à l'aide d'une bibliothèque SD-JWT.
  2. Validez les valeurs attendues et les revendications de session.
  3. Vérifiez la délégation DNS.
  4. Découvrez les métadonnées de l'émetteur et récupérez les JWKS.
  5. Vérifiez les signatures cryptographiques et l'association de clés.

1. Analyser le jeton

Le jeton utilise le format RFC 9901 : Selective Disclosure JWT (SD-JWT+KB). Utilisez les bibliothèques appropriées pour votre plate-forme afin d'analyser et de valider le jeton. Par exemple, pour Node, vous pouvez utiliser @sd-jwt/core et jose. Sous sa forme brute, il se présente comme suit : un jeton JWT signé par un émetteur, suivi d'une ou plusieurs divulgations, et se terminant par un jeton JWT de liaison de clé, chaque composant étant séparé par un tilde :

<Issuer-signed EVT>~<Disclosure 1>~...~<Disclosure N>~<Key Binding JWT>

Dans l'implémentation actuelle, le jeton ne contient aucune divulgation (<Issuer-signed EVT>~<Key Binding JWT>). Toutefois, cela peut changer à l'avenir.

Décodez le jeton avec la bibliothèque :

import { decodeSdJwtSync } from "@sd-jwt/core";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
  createHash(alg === "sha-256" ? "sha256" : alg)
    .update(data)
    .digest();
const decoded = decodeSdJwtSync(rawToken, hasher);
const evtPayload = decoded.jwt.payload;
const kbPayload = decoded.kbJwt?.payload;

Si verifier.example valide demo@provider.example, le jeton décodé ressemble à ce qui suit :

{
  "evtJwtDecodedHeader": {
    "typ": "evt+jwt",
    "alg": "EdDSA",
    "kid": "issuer-key-id"
  },
  "evtJwtDecodedPayload": {
    "iss": "https://provider.example",
    "iat": 12345678901,
    "exp": 12345679901,
    "cnf": {
      "jwk": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x": "pUbLiCkEy123pUbLiCkEy123pUbLiCkEy123"
      }
    },
    "email": "demo@provider.example",
    "email_verified": true
  },
  "kbJwtDecodedHeader": {
    "alg": "EdDSA",
    "typ": "kb+jwt"
  },
  "kbJwtDecodedPayload": {
    "aud": "https://verifier.example",
    "iat": 12345678901,
    "nonce": "rAnDoM123rAnDoM123rAnDoM123rAnDoM123",
    "sd_hash": "hAsH456hAsH456hAsH456hAsH456hAsH456"
  },
  "disclosures": []
}

2. Valider les valeurs attendues et les revendications de session

Vérifiez que les valeurs de base de la charge utile correspondent aux valeurs que vous avez fournies et attendues :

  • email_verified : doit être true.
  • email : doit correspondre à l'adresse e-mail indiquée dans le formulaire.
  • aud (audience) : doit correspondre à l'origine de votre site.
  • nonce : doit correspondre au nonce fourni dans votre formulaire.
  • iat (émis le) et exp (expiration) : vérifiez que le jeton est dans sa période de validité et qu'il n'a pas expiré.

3. Vérifier la délégation DNS

Vérifiez l'enregistrement DNS _email-verification pour le domaine de l'adresse e-mail. Par exemple, pour demo@gmail.com, interrogez l'enregistrement TXT _email-verification.gmail.com. Pour ce fournisseur, la requête renvoie l'emplacement du fournisseur de compte, c'est-à-dire accounts.google.com.

$ dig +short TXT _email-verification.gmail.com
"iss=accounts.google.com"

Vérifiez que le schéma de l'émetteur est https:// et que https://<domain> correspond à la revendication iss dans l'EVT.

4. Vérifier la signature EVT

Récupérez les métadonnées de découverte de l'émetteur à partir de https://<issuer>/.well-known/email-verification :

{
  "issuance_endpoint": "https://accounts.google.com/gsi/email-verification/issue",
  "jwks_uri": "https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks",
  "signing_alg_values_supported": ["EdDSA"]
}

Récupérez le jeu de clés Web JSON à partir de jwks_uri.

Utilisez votre bibliothèque SD-JWT pour valider le package de jetons. La bibliothèque coordonne la validation :

  1. Validation de la signature de l'émetteur sur l'EVT par rapport aux JWKS récupérés.
  2. Valider la signature du navigateur sur le KB-JWT à l'aide de la clé publique éphémère dans cnf.jwk.
  3. Vérification de la liaison de clé (aud, nonce et hachage du résumé sd_hash).

Exemple de logique de validation dans Node.js :

import { SDJwtInstance } from "@sd-jwt/core";
import { importJWK, compactVerify } from "jose";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
  createHash(alg === "sha-256" ? "sha256" : alg)
    .update(data)
    .digest();
const sdJwt = new SDJwtInstance({ hasher });
sdJwt.config({
  hasher,
  // Verifier for the Issuer-signed EVT
  verifier: async (data, sig) => {
    const token = `${data}.${sig}`;
    const header = decoded.jwt.header;
    const headerAlg = header.alg || "ES256";
    // Match by kid if present, or iterate across matching algorithm keys
    const keysToTry = header.kid
      ? jwksData.keys.filter(k => k.kid === header.kid)
      : jwksData.keys;
    for (const jwk of keysToTry) {
      try {
        const pubKey = await importJWK(jwk, jwk.alg || headerAlg);
        await compactVerify(token, pubKey);
        return true;
      } catch {
        // Try next candidate key
      }
    }
    return false;
  },
  // Verifier for the Key Binding JWT (KB-JWT)
  kbVerifier: async (data, sig) => {
    try {
      const browserJwkKey = evtPayload.cnf?.jwk;
      if (!browserJwkKey) return false;
      const pubKey = await importJWK(browserJwkKey, decoded.kbJwt.header.alg || "ES256");
      await compactVerify(`${data}.${sig}`, pubKey);
      return true;
    } catch {
      return false;
    }
  },
});
// The library automatically verifies EVT signature, KB-JWT signature, audience, nonce, and sd_hash
const result = await sdJwt.verify(rawToken, {
  kb: {
    expectedNonce: sessionNonce,
    expectedAudience: "https://example.com",
    required: true,
  },
});
const verifiedPayload = result.payload;

Si toutes les étapes aboutissent, vous avez validé l'adresse e-mail auprès du fournisseur. Si la validation échoue, revenez à l'envoi d'un e-mail de confirmation à l'utilisateur à l'aide de votre flux habituel.

Dépannage

Si la validation échoue ou si le navigateur ne fournit pas de jeton, vérifiez les problèmes courants suivants :

Le champ de jeton est vide lors de l'envoi

  • Inscription à l'essai Origin Trial : vérifiez que l'en-tête Origin-Trial ou la balise <meta> sont diffusés sur la page. Pour les essais Origin Trial tiers (Chrome 154 et versions ultérieures), l'origine de l'essai Origin Trial enregistré doit être de même origine que l'émetteur (https://<issuer-domain>). Vous pouvez inspecter la configuration de l'essai Origin Trial sur un site dans les Outils de développement sous Application > Frames > (sélectionnez le frame concerné) > Origin trials.
  • Balises de formulaire : <input type="email" autocomplete="email"> et <input type="hidden" autocomplete="email-verification-token" nonce="..."> doivent se trouver dans le même élément <form> (et non isolés au-delà des limites du Shadow DOM), et nonce ne doit pas être vide.
  • Envoi anticipé ou page réutilisée : le navigateur récupère le jeton en arrière-plan après la saisie ou l'autosaisie de l'adresse e-mail. Si vous envoyez la requête avant qu'elle ne soit terminée, le jeton reste vide. Cela peut se produire si l'utilisateur appuie sur "Retour" pour envoyer le formulaire après avoir saisi son adresse e-mail.
  • Conditions préalables concernant le navigateur et le fournisseur : l'utilisateur doit être connecté à un fournisseur participant dans le même profil de navigateur et avoir activé l'adresse e-mail validée dans les paramètres Chrome (chrome://settings/contactInfo).

Échec de la validation de la signature de l'émetteur

  • En-tête kid manquant : la revendication kid (ID de clé) dans l'en-tête EVT et le JWKS est facultative (par exemple, Gmail omet kid). Si kid est absent, parcourez toutes les clés candidates dans le jwks_uri de l'émetteur au lieu d'échouer lors d'une recherche d'ID de clé.
  • Identifiants d'algorithme (EdDSA et Ed25519) : les émetteurs et les bibliothèques peuvent spécifier EdDSA ou Ed25519 (avec ES256). Assurez-vous que votre logique d'importation et de validation JWK accepte les deux identifiants.
  • Format d'origine de l'émetteur (iss) : l'enregistrement TXT DNS (_email-verification.<domain>) contient un nom d'hôte nu (iss=accounts.issuer.example), tandis que la revendication EVT iss est une origine HTTPS complète (https://accounts.issuer.example, sans barre oblique à la fin). Ajoutez le préfixe https:// à la valeur de l'enregistrement DNS avant de la comparer.

Échec de la validation de la liaison de clé (KB-JWT)

  • Nonce non concordant ou expiré : assurez-vous que le nonce affiché dans <input> correspond au nonce de session actif sur votre serveur et qu'il n'a pas été remplacé par un autre onglet ni utilisé par une requête précédente.
  • Incohérence de l'audience (aud) : la revendication aud correspond à l'origine HTTPS du validateur (https://verifier.example, sans chemin d'accès ni barre oblique de fin).

Échec de la comparaison de la revendication par e-mail (email)

  • Casse et canonisation : Chrome 156 et versions ultérieures renvoient la revendication email octet par octet telle qu'elle a été saisie dans le formulaire, mais les versions antérieures du navigateur ou les fournisseurs peuvent renvoyer une adresse canonisée (par exemple, First.Last@example.com pour first.last@example.com). Utilisez une comparaison insensible à la casse lorsque vous faites correspondre la revendication email du jeton à la valeur du formulaire envoyé.