EZOUEU
Sécurité · Ed25519

Reconstruire exactement les octets signés

L’attestation est un objet JSON contenant une déclaration, une signature Ed25519 et la clé publique brute encodées en base64.

Format : ezou-deletion-proof/1Vérifié le

Enveloppe téléchargée

  • proof : la déclaration dont les octets JSON sont signés.
  • signature : signature Ed25519 en base64 standard.
  • public_key : clé publique Ed25519 brute de 32 octets, en base64 standard.
  • algorithm : valeur déclarée ed25519.

Champs de la déclaration

version
Version du format, actuellement ezou-deletion-proof/1.
site_id
Identifiant interne de la publication.
slug_sha256
SHA-256 hexadécimal du slug en clair, afin de ne pas publier le secret d’une URL non listée.
reason
requested pour une suppression demandée ou expired pour le worker d’expiration.
files_deleted
Nombre d’objets supprimés du préfixe de stockage.
bytes_deleted
Volume total réel enregistré lors de la finalisation.
storage_purged_at
Instant UTC relevé après la suppression du stockage principal.
cdn_purged_at
Instant UTC relevé après l’appel au purger configuré.
cdn_provider
Nom du purger, none dans l’architecture de production sans CDN.
issuer
Émetteur déclaré, ezou.eu.
key_id
16 caractères hexadécimaux : les huit premiers octets du SHA-256 de la clé publique.
issued_at
Instant UTC auquel le service a complété puis signé la déclaration.

Sérialisation canonique utilisée

Le format n’emploie pas RFC 8785. Le binaire Go sérialise la structure proof sans indentation, dans l’ordre de déclaration des champs ci-dessous, avec les règles de encoding/json. Les instants sont les chaînes produites par time.Time. La signature porte uniquement sur ce JSON compact, pas sur l’enveloppe extérieure.

{
  "version": "ezou-deletion-proof/1",
  "site_id": "s_…",
  "slug_sha256": "64 caractères hexadécimaux",
  "reason": "requested",
  "files_deleted": 3,
  "bytes_deleted": 12345,
  "storage_purged_at": "2026-08-21T10:00:00Z",
  "cdn_purged_at": "2026-08-21T10:00:00Z",
  "cdn_provider": "none",
  "issuer": "ezou.eu",
  "key_id": "16 caractères hexadécimaux",
  "issued_at": "2026-08-21T10:00:00Z"
}

Les espaces et retours à la ligne de l’exemple sont ajoutés pour la lecture. Les octets signés correspondent à la version compacte produite par JSON.stringify(proof) si l’ordre des propriétés du JSON reçu est conservé.

Procédure de vérification

  1. Vérifier que algorithm vaut ed25519 et que la version est prise en charge.
  2. Récupérer la clé publiée sur /.well-known/ezou-deletion-key et refuser l’attestation si sa public_key en diffère. Cette étape vient d’abord : elle est la seule qui rattache la déclaration à EZOU.EU.
  3. Décoder la clé de confiance et signature depuis le base64 standard.
  4. Sérialiser proof en JSON compact dans l’ordre documenté.
  5. Vérifier la signature Ed25519 sur ces octets, avec la clé publiée et non avec celle que l’attestation transporte.
  6. Recalculer le key_id depuis la clé publiée et le comparer au champ signé.
  7. Si le slug est connu, comparer SHA-256(slug) à slug_sha256.

L’ordre compte. Vérifier la signature avec la clé que l’attestation fournit ne répond qu’à « ce fichier a-t-il été modifié ? » : n’importe qui peut générer une paire Ed25519, signer une déclaration inventée, y embarquer sa clé publique et son key_id recalculé, et obtenir une vérification satisfaite de bout en bout. Le script ci-dessous compare donc la clé avant de vérifier quoi que ce soit, et échoue avec un code de sortie non nul dès la première étape qui ne passe pas.

import { createHash, verify } from "node:crypto";
import { promises as fs } from "node:fs";

const KEY_URL = "https://api.ezou.eu/.well-known/ezou-deletion-key";
const signed = JSON.parse(await fs.readFile("attestation.json", "utf8"));

if (signed.algorithm !== "ed25519") {
  throw new Error("algorithme non pris en charge : " + signed.algorithm);
}

// 1. La clé de confiance est celle publiée par EZOU.EU — jamais celle que
// l'attestation transporte. Sans cette étape, une attestation forgée et
// auto-signée passerait la vérification : elle fournirait sa propre clé.
const published = await fetch(KEY_URL).then((r) => r.json());
if (signed.public_key !== published.public_key) {
  throw new Error("clé inconnue : cette attestation ne vient pas d'EZOU.EU");
}

// 2. Ensuite seulement, la signature — vérifiée avec la clé publiée.
const publicKeyRaw = Buffer.from(published.public_key, "base64");
const canonical = Buffer.from(JSON.stringify(signed.proof));
const signature = Buffer.from(signed.signature, "base64");

// Une clé Ed25519 brute doit être enveloppée en SPKI pour node:crypto.
const spkiPrefix = Buffer.from("302a300506032b6570032100", "hex");
const publicKey = Buffer.concat([spkiPrefix, publicKeyRaw]);
if (!verify(null, canonical, {
  key: publicKey,
  format: "der",
  type: "spki"
}, signature)) {
  throw new Error("signature invalide : déclaration modifiée ou octets non canoniques");
}

// 3. Le key_id fait partie des octets signés : le recalculer depuis la clé
// publiée détecte une preuve signée par une autre clé du même émetteur.
const keyID = createHash("sha256").update(publicKeyRaw).digest("hex").slice(0, 16);
if (keyID !== signed.proof.key_id) {
  throw new Error("key_id incohérent avec la clé publiée");
}

console.log("authentique", signed.proof.site_id, signed.proof.issued_at);

Clé publique de confiance

La clé active est publiée en JSON sur /.well-known/ezou-deletion-key avec son algorithme et son key_id.

Rotation de clé

L’endpoint public expose aujourd’hui la clé active, pas un historique signé des anciennes clés. Une attestation ancienne peut vérifier sa propre signature avec la clé qu’elle embarque, mais l’attribution durable à EZOU.EU après rotation exige d’avoir conservé la clé publique ou une copie datée de l’endpoint. Ce point reste à durcir avant de présenter la rotation comme entièrement vérifiable.

Portée de la vérification

Une signature valide montre que la déclaration n’a pas été modifiée depuis sa signature par la clé considérée. La comparaison à une clé EZOU.EU de confiance attribue cette déclaration au service. Elle ne prouve pas l’absence de copies chez un destinataire, dans un système tiers ou dans une sauvegarde non couverte par les champs signés.