Tout ce qui suit fonctionne tel quel. Les empreintes sont celles de l’implémentation de référence (riftveil.ai).
1. Calculer une empreinte
L’entrée d’empreinte est un objet JSON sérialisé sans espaces, sans échappement des caractères non ASCII, avec les clés dans l’ordre indiqué. En JavaScript, c’est exactement JSON.stringify. En Python, utilisez ensure_ascii=False et separators=(",", ":"). Calculez ensuite le SHA-256 des octets UTF-8, en hexadécimal minuscule.
| Profil | Objet d’entrée, dans cet ordre de clés |
|---|---|
riftveil-report-v1 |
v (1), id, protocol, model, project, context, report |
riftveil-content-v1 |
v (1), assistant, project, context, report |
riftveil-decision-v1 |
v (1), id, report_fingerprint, verified_by {last_name, first_name, role}, checked, questions [{n, question, suggested_owner, assigned_to}], decision, conditions, place, signature {method, typed, image} |
riftveil-decision-v2 |
les champs de riftveil-decision-v1 avec v (2), puis final_document {name, size, sha256}, previous {fingerprint, seal, sealed_at} |
JavaScript (navigateur ou Node 18 et plus) :
async function sha256hex(text) {
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
return [...new Uint8Array(d)].map(b => b.toString(16).padStart(2, "0")).join("");
}
function reportInput(f) {
return JSON.stringify({ v: 1, id: f.record_id, protocol: f.assessment.protocol, model: f.assessment.model,
project: f.content.project, context: f.content.context, report: f.content.report });
}
const file = JSON.parse(recordFileText);
const fingerprint = await sha256hex(reportInput(file));
if (fingerprint !== file.proof.fingerprint.value) throw new Error("content altered");
Python 3 :
import hashlib, json
def canon(obj):
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
def report_fingerprint(f):
a, c = f["assessment"], f["content"]
obj = {"v": 1, "id": f["record_id"], "protocol": a["protocol"], "model": a["model"],
"project": c["project"], "context": c["context"], "report": c["report"]}
return hashlib.sha256(canon(obj).encode("utf-8")).hexdigest()
f = json.load(open("Riftveil RV-….json", encoding="utf-8"))
assert report_fingerprint(f) == f["proof"]["fingerprint"]["value"], "content altered"
2. Contrôler la signature avec la clé publique
Version 0.1.1. La signature est une signature Ed25519 du message du sceau ; chacun peut la contrôler avec la liste des clés publiée de l’émetteur, sans rien lui demander. Prenez la liste des clés à l’adresse enregistrée pour le préfixe de l’émetteur dans ISSUERS, jamais dans proof.signature.keys : ce champ est écrit dans le fichier, si bien qu’un fichier falsifié pourrait désigner une fausse liste.
JavaScript (navigateurs récents, Node 20 et plus) :
const b64u = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
const label = file.origin === "registered" ? "riftveil-register-v1" : "riftveil-seal-v1";
const message = label + "|" + file.record_id + "|" + fingerprint; // fingerprint recomputed in step 1
const KEY_LISTS = { RV: "https://riftveil.ai/keys.json" }; // registered addresses, see ISSUERS
const listUrl = KEY_LISTS[file.record_id.split("-")[0]];
if (!listUrl) throw new Error("unknown issuer");
const list = await (await fetch(listUrl)).json();
const k = list.keys.find((x) => x.id === file.proof.signature.key && x.alg === "ed25519");
if (!k || k.status === "compromised") throw new Error("key unknown or compromised");
const pub = await crypto.subtle.importKey("raw", b64u(k.public_key), { name: "Ed25519" }, false, ["verify"]);
const ok = await crypto.subtle.verify({ name: "Ed25519" }, pub, b64u(file.proof.signature.value), new TextEncoder().encode(message));
if (!ok) throw new Error("signature not valid");
Python 3 (pip install cryptography) :
import base64, json, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def b64u(s): return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
KEY_LISTS = {"RV": "https://riftveil.ai/keys.json"} # registered addresses, see ISSUERS
fingerprint = report_fingerprint(f) # recomputed in step 1 (for a registered report, use its own profile)
label = "riftveil-register-v1" if f["origin"] == "registered" else "riftveil-seal-v1"
message = label + "|" + f["record_id"] + "|" + fingerprint
sig = f["proof"]["signature"]
keys = json.load(urllib.request.urlopen(KEY_LISTS[f["record_id"].split("-")[0]]))["keys"]
k = next(k for k in keys if k["id"] == sig["key"] and k["alg"] == "ed25519")
assert k["status"] != "compromised", "key compromised"
Ed25519PublicKey.from_public_bytes(b64u(k["public_key"])).verify(b64u(sig["value"]), message.encode("utf-8")) # raises if not valid
Pour un suivi, le message est "riftveil-decision-v1|" + id + "|" + follow-up fingerprint + "|" + sealed_at, et la signature est follow_up.proof.signature. Pour un suivi antérieur sous previous, recommencez avec ses propres champs.
3. Faire vérifier le sceau par l’émetteur
Le sceau est un HMAC calculé avec la clé secrète de l’émetteur : seul l’émetteur peut le vérifier. N’envoyez que des empreintes.
const res = await fetch("https://riftveil.ai/api/verify", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: file.record_id, fingerprint, seal: file.proof.seal.value })
});
const r = await res.json(); // { valid: true, id, issuedAt, origin } or { valid: false }
if (!r.valid) throw new Error("seal not valid");
if (r.origin !== file.origin) throw new Error("forged origin");
Pour un fichier avec un suivi, ajoutez decision: { fingerprint, seal, sealedAt } à la même requête ; la réponse contient alors aussi decision: { valid, sealedAt }.
curl -s https://riftveil.ai/api/verify -H 'Content-Type: application/json' \
-d '{"id":"RV-…","fingerprint":"…64 hex…","seal":"S1-…"}'
4. Valider la structure
pip install jsonschema
python3 -c "import json,jsonschema; jsonschema.validate(json.load(open('record.json')), json.load(open('attestscope-0.1.1.schema.json')))"
Les fichiers de la version 0.1 sont valides selon assay-record-0.1.schema.json.
5. Vecteurs de test
Calculés avec l’implémentation de référence. Les sceaux utilisent la clé de test publique assay-test-key-1, les signatures la clé de test Ed25519 publique ed25519-1 de test-vectors.json (graine SHA-256("attestscope-test-ed25519-1"), clé publique 2FLWc-QnkT78GPsymtkuE5xgzIMLCHzigAAgOs6D2TY), et le préfixe XT, réservé aux tests : ils permettent de tester une implémentation du scellement ou de la signature, et ne sont jamais valides sur riftveil.ai. Entrées complètes : test-vectors.json. La trace d’illustration examples/elena-lambert.json utilise les mêmes clés de test, avec la liste des clés test-keys.json.
| Vecteur | Empreinte | Sceau avec la clé de test |
|---|---|---|
| V1 · généré (riftveil-report-v1) | abc5ba903a307668121b1fdd0e01ce784069189c1b255e481f65f5e54ee765fc |
S1-2ZQR-3N0J-PRKP-8G33-Q2ZT-ZPZA-QDFN-MEPH |
| V2 · enregistré (riftveil-content-v1) | 2ae571cf5ba080714e2d04e867a05b09a36f4157ddbdd17d3a1574366af0f9fd |
S1-7CBT-7WG0-0HBP-2336-45NX-2PDS-HE57-C6DK |
| V3 · texte non ASCII et saut de ligne | 14971ae9adf597868bd4ab39783cc64c7ebff23e38d7a4f2f80d11df9d212f55 |
S1-CW1S-3W97-PB70-MKYQ-NTD5-JFQK-P8ES-B883 |
| V4 · suivi (riftveil-decision-v1), lié à V1 | bfdb75b1260feca991f6f59862c3cdc7e3910f424612244e9fdeb999a951d82e |
S1-FEK5-BRSD-MC99-S95R-2V46-C40R-CHYM-ARK2 |
| V6 · suivi (riftveil-decision-v2), complète V4, avec un document final | 2f283adb5660de3e785220c90a27a7eb9abe43fb310dffbdda579d6b2f87b817 |
S1-JZHS-ME8Y-5A25-4S9Z-7YBJ-HMYE-7J26-B9AT |
Signatures avec la clé de test Ed25519 : V5, sur le message du sceau de V1, 2jKXUE5glnWjDwQg-u4d6-GiOJKflAYbj3lgjDrsqunL4gs6TOentYbyUpIbbFX7IYIZ2qGmHYvVtpVAyI9BDg ; V6, sur le message de son sceau, kWyKO9MpoYuqXmawQUVZZH__4k3rWgWIyHo5mTNIB3w_mIiZqTLRv4sEAn4GGnbBWt5BUPn7jhNwt95aa8nTCw.
Entrée de V1, octet pour octet :
{"v":1,"id":"XT-260101-000000-0000-0000-0000","protocol":"1.3.3","model":"test-model","project":"Project text.","context":"","report":"Report text."}
Sceau : "S1-" suivi des 160 premiers bits de HMAC-SHA256(key, "riftveil-seal-v1|" + id + "|" + fingerprint), en base32 de Crockford (0123456789ABCDEFGHJKMNPQRSTVWXYZ), soit 32 caractères en 8 groupes de 4. Pour V1, l’entrée du HMAC est riftveil-seal-v1|XT-260101-000000-0000-0000-0000|abc5ba903a307668121b1fdd0e01ce784069189c1b255e481f65f5e54ee765fc.
6. Ajouter la vérification à une page
<div data-riftveil-verify data-lang="en"></div>
<script src="https://riftveil.ai/embed/verify.js" defer></script>
La politique de sécurité du contenu (en-tête Content-Security-Policy) de la page qui accueille le module doit autoriser script-src et connect-src https://riftveil.ai.
7. Devenir émetteur
Inscrivez un préfixe dans ISSUERS (préfixe, nom, contact, point d’accès de vérification, adresse de la liste des clés), publiez vos profils d’empreinte si vous en ajoutez, créez vous-même votre clé de signature et gardez-la, comme votre clé de scellement, dans un coffre de secrets, publiez votre liste des clés, et suivez les règles de l’émetteur de la section Conformité.