AttestScopeFR

Implementer guide

Write and verify record files

Code that runs as is, in JavaScript and Python, with test vectors computed by the reference implementation.

Version 0.1.1 · Published 26 October 2026 · Reference implementation Riftveil · Publisher Riftveil

Technical diagram: what is computed, what travels, what stays

Version 0.1.1 (26 October 2026), in service on riftveil.ai, with the Ed25519 signature. The records of version 0.1, where the format is still called assay-record, stay readable.

Go to the text of the implementer guide

Issuer · riftveil.ai Signer

Browserof the person who signsriftveil.aiserver functionsModelproviderAny verifierlater, anyoneLater · anyone can verify1 · project · context · assessment2 · ID assigned by codeUTC clock, to the second+ 60 random bitsnever by the model3 · protocol (instructions)project · context· one run4 · report5a · Fingerprint, seal, signatureriftveil-report-v1S1-… · riftveil-seal-v1nothing is kept5 · report · ID(X-Riftveil-Record, eventrecord) · fingerprint · seal S1-…6 · The person fills in the follow-upnever a modelfingerprint computed here:riftveil-decision-v17 · POST /api/decide{ id, report_fingerprint,report_seal, fingerprint }8a · Report seal checkedthen follow-up seal,with the server timenothing is kept8 · { seal, sealedAt, signature }HMAC riftveil-decision-v1|id|fingerprint|sealed_at9 · Record file assembledRiftveil <ID>.jsonnothing is sentit stays with the personThe person hands over the filenever a PDF alone: IDand seal can be copied ontoan altered document10 · Recomputes the fingerprintsfrom the file, followingthe profile it names; neverfrom the written fingerprint10 · POST /api/verify{ id, fingerprint, seal, decision? }{ valid, id, issuedAt, origin, decision? }Result shownvalid · content altered · seal not valid ·forged originBrowser of the person who signsriftveil.ai · server functionsModel providerAny verifier, laterBrowserriftveil.ai1 · project · context · assessmentriftveil.ai2 · ID assigned by codeUTC clock, to the second+ 60 random bitsnever by the modelriftveil.aiModel provider3 · protocol (instructions)project · context · one runModel providerriftveil.ai4 · reportriftveil.ai5a · Fingerprint, seal, signatureriftveil-report-v1S1-… · riftveil-seal-v1nothing is keptriftveil.aiBrowser5 · report · ID(X-Riftveil-Record, event record)fingerprint · seal S1-…Browser6 · The person fills in the follow-upnever a modelfingerprint computed here:riftveil-decision-v1Browserriftveil.ai7 · POST /api/decide{ id, report_fingerprint,report_seal, fingerprint }riftveil.ai8a · Report seal checkedthen follow-up seal, with the servertime · nothing is keptriftveil.aiBrowser8 · { seal, sealedAt }follow-up seal and signatureHMAC riftveil-decision-v1|id|fingerprint|sealed_atBrowser9 · Record file assembledRiftveil <ID>.jsonnothing is sentit stays with the personLater · anyone can verifyBrowserThe person hands over the filenever a PDF alone: ID andseal can be copied onto an altereddocumentAny verifier10 · Recomputes the fingerprintsfrom the file, following the profileit names; never fromthe written fingerprintAny verifierriftveil.ai10 · POST /api/verify{ id, fingerprint, seal,decision? }riftveil.aiAny verifier{ valid, id, issuedAt,origin, decision? }Any verifierResult shownvalid · content altered ·seal not valid · forged origin
The exchanges between the browser, riftveil.ai and the verifier.

Today“Register a report” page

Report made in another AI assistant

The protocol was applied in Microsoft 365 Copilot, ChatGPT, Claude, Gemini, Mistral Vibe or another AI assistant. The person pastes the project and the report on riftveil.ai.

  1. a · The browser computes the fingerprintriftveil-content-v1 { v: 1, assistant, project, context, report }
  2. b · Only the fingerprint is sentPOST /api/register { fingerprint, profile }
  3. c · riftveil.ai returns ID and seal{ id, seal, issuedAt, origin: "registered", profile } label riftveil-register-v1

Origin shown: “registered with Riftveil”, distinct from “generated”. The seal proves that this exact text existed on that date and has not changed since; applying the protocol properly is the responsibility of the person who registers the report.

The follow-up of a registered report is filled in, sealed and signed like that of a report generated on riftveil.ai.

Endpoints

In service · version 0.1.1

  • POST /api/verify{ id, fingerprint, seal, signature?, decision? } → { valid: true, id, issuedAt, origin, signature?, decision? } or { valid: false }

    Open to any site · receives fingerprints only · keeps nothing.

  • POST /api/register{ fingerprint, profile: "riftveil-content-v1" } → { id, seal, signature, issuedAt, origin: "registered", profile }
  • POST /api/decide{ id, report_fingerprint, report_seal, fingerprint } → { seal, sealedAt, origin, signature, report_signature }
  • GET /keys.json→ { issuer, updated, keys: [ { id, alg, public_key, status, valid_from, … } ] }

    The list of riftveil.ai’s public keys, open to any site. No key is removed from it.

Registration and follow-up: from the riftveil.ai pages, behind a bot check; only a daily counter per hashed IP address is kept.

Registered prefixes: https://riftveil.ai/issuers.json

Widget for another site: it verifies the report, its signature and the follow-up seal; the follow-up itself is read on riftveil.ai/verify. 6. Add verification to a page

The computation rules

ID

XTissuer-261026UTC date-143205UTC time-7K3M-Q9XD-2B8F60 random bits

<ISSUER>-<YYMMDD>-<HHMMSS>-<RRRR>-<RRRR>-<RRRR>^[A-Z]{2,6}-\d{6}-\d{6}-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$
  • Assigned by code at the moment the record is created, from a clock synchronised with a public time source.
  • Never produced, suggested or completed by a model.
  • Independent of the content: two assessments of the same project make two records.
  • Unique: a new version gets a new ID.
  • Crockford base32: 0-9 and A-Z without I, L, O, U; 12 characters in 3 groups of 4.
  • RV: Riftveil. XT: reserved for tests, never for a decision.

Fingerprints · SHA-256, 64 characters

riftveil-report-v1Report generated on riftveil.ai

riftveil-content-v1Report made elsewhere, registered

riftveil-decision-v2Follow-up and decision (0.1.1), with the final document and the previous follow-up, every field present

riftveil-decision-v1Follow-up and decision (0.1)

1. Compute a fingerprint

Every verifier recomputes the fingerprint from the file. A single changed character changes the whole fingerprint, from which the text cannot be recovered. The follow-up contains the report fingerprint: it belongs to one report only.

Seal · HMAC-SHA256

S1-REQ8-RW0G-J013-M23D-C969-73JZ-MF93-1W6J

S1: key no. 1 · 32 base32 characters: 160 bits

5. Test vectors · 3. Check the seal with the issuer

  • riftveil-seal-v1generated on riftveil.ai
  • riftveil-register-v1registered, made elsewhere
  • riftveil-decision-v1follow-up; the message adds |sealed_at
  • One label per origin: a seal can never pass for the other origin.
  • Secret key in a secret store, never in code, a shared file or a conversation.
  • Numbered keys: a new key gets a new number, the old ones stay available for verification.

Ed25519 signature on the same message, with another key (ed25519-1): 86 base64url characters, which anyone checks with the public key. 2. Check the signature with the public key

A valid seal proves that riftveil.ai assigned this ID, at the time given by its clock, to this exact content. Judging whether the project is right falls to the person who signs.

Never leaves the device

  • The names of the person who signs (verified_by) and of the people the questions are assigned to
  • The decision and its conditions
  • The signature, drawn on screen or typed
  • The final document: only its fingerprint, computed on the device, goes into the follow-up
  • The record file itself
What goes to riftveil.ai

For the assessment: the project and the context, for a single run at the model provider; riftveil.ai keeps none of it.

To seal the follow-up, register, verify: IDs, fingerprints, seals, signatures and times, nothing else.

Whoever holds the record file holds the texts and the names in the follow-up: it is shared as you would share the report.

The versions of the format

Version 0.1.1 · in service

Public-key signature

  • Specification published on GitHub, like the Riftveil protocol v1.0.0.
  • Ed25519 signature of every report and every follow-up by riftveil.ai, on the same message as the seal.
  • Public keys published on GitHub, on the format’s site and at riftveil.ai/keys.json.
  • Check with riftveil.ai’s public key: anyone can redo it, without going through riftveil.ai.
  • Final document, follow-up completed later, follow-up of registered reports.
  • Optional field proof.timestamps: one or more RFC 3161 tokens (an optional qualified time stamp, or the organisation’s own authority).

Version 0.2Planned

Independent proof and register

  • Canonical JSON (RFC 8785) as a fingerprint input, alongside the current profiles.
  • Register entry: the record without the content, for the organisation register.
  • Per-finding follow-up, outcome and readers[], each with its own fingerprint.
  • Organisation rule set and rule-by-rule attestation.
  • Public log.

Transition rules

Everything issued stays readable

  • The computation of an existing profile stays the same in every new version.
  • A 0.1.1 verifier verifies every 0.1 record; a 0.2 verifier will verify every 0.1 and 0.1.1 record.
  • In 0.1, files carry "format": "assay-record"; since 0.1.1, "format": "attestscope". The records already issued stay readable.

Everything below runs as is. The fingerprints are those of the reference implementation (riftveil.ai).

1. Compute a fingerprint

The fingerprint input is a JSON object serialised without spaces, without escaping non-ASCII characters, keys in the order shown. In JavaScript this is exactly JSON.stringify. In Python, use ensure_ascii=False and separators=(",", ":"). Then SHA-256 over the UTF-8 bytes, in lower-case hexadecimal.

Profile Input object, in this key order
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 the fields of riftveil-decision-v1 with v (2), then final_document {name, size, sha256}, previous {fingerprint, seal, sealed_at}

JavaScript (browser or Node 18+):

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. Check the signature with the public key

Version 0.1.1. The signature is Ed25519 over the seal message; anyone can check it with the issuer's published key list, without asking the issuer. Take the key list from the address registered for the issuer's prefix in ISSUERS, never from proof.signature.keys: that field is written in the file, so a forged file could point to a forged list.

JavaScript (recent browsers, Node 20+):

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

For a follow-up, the message is "riftveil-decision-v1|" + id + "|" + follow-up fingerprint + "|" + sealed_at, and the signature is follow_up.proof.signature. For an earlier follow-up under previous, repeat with its own fields.

3. Check the seal with the issuer

The seal is an HMAC with the issuer's secret key: only the issuer can check it. Send fingerprints only.

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");

For a file with a follow-up, add decision: { fingerprint, seal, sealedAt } to the same request; the answer adds decision: { valid, sealedAt }.

curl -s https://riftveil.ai/api/verify -H 'Content-Type: application/json' \
  -d '{"id":"RV-…","fingerprint":"…64 hex…","seal":"S1-…"}'

4. Validate the 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')))"

Version 0.1 files validate against assay-record-0.1.schema.json.

5. Test vectors

Computed with the reference implementation. The seals use the public test key assay-test-key-1, the signatures the public Ed25519 test key ed25519-1 of test-vectors.json (seed SHA-256("attestscope-test-ed25519-1"), public key 2FLWc-QnkT78GPsymtkuE5xgzIMLCHzigAAgOs6D2TY), and the prefix XT, reserved for tests: they let you test a sealing or signing implementation, and are never valid at riftveil.ai. Full inputs: test-vectors.json. The illustrative record examples/elena-lambert.json uses the same test keys, with the key list test-keys.json.

Vector Fingerprint Seal with the test key
V1 · generated (riftveil-report-v1) abc5ba903a307668121b1fdd0e01ce784069189c1b255e481f65f5e54ee765fc S1-2ZQR-3N0J-PRKP-8G33-Q2ZT-ZPZA-QDFN-MEPH
V2 · registered (riftveil-content-v1) 2ae571cf5ba080714e2d04e867a05b09a36f4157ddbdd17d3a1574366af0f9fd S1-7CBT-7WG0-0HBP-2336-45NX-2PDS-HE57-C6DK
V3 · non-ASCII text and line break 14971ae9adf597868bd4ab39783cc64c7ebff23e38d7a4f2f80d11df9d212f55 S1-CW1S-3W97-PB70-MKYQ-NTD5-JFQK-P8ES-B883
V4 · follow-up (riftveil-decision-v1), bound to V1 bfdb75b1260feca991f6f59862c3cdc7e3910f424612244e9fdeb999a951d82e S1-FEK5-BRSD-MC99-S95R-2V46-C40R-CHYM-ARK2
V6 · follow-up (riftveil-decision-v2), completes V4, with a final document 2f283adb5660de3e785220c90a27a7eb9abe43fb310dffbdda579d6b2f87b817 S1-JZHS-ME8Y-5A25-4S9Z-7YBJ-HMYE-7J26-B9AT

Signatures with the Ed25519 test key: V5, over the seal message of V1, 2jKXUE5glnWjDwQg-u4d6-GiOJKflAYbj3lgjDrsqunL4gs6TOentYbyUpIbbFX7IYIZ2qGmHYvVtpVAyI9BDg; V6, over its seal message, kWyKO9MpoYuqXmawQUVZZH__4k3rWgWIyHo5mTNIB3w_mIiZqTLRv4sEAn4GGnbBWt5BUPn7jhNwt95aa8nTCw.

V1 input, byte for byte:

{"v":1,"id":"XT-260101-000000-0000-0000-0000","protocol":"1.3.3","model":"test-model","project":"Project text.","context":"","report":"Report text."}

Seal: "S1-" + the first 160 bits of HMAC-SHA256(key, "riftveil-seal-v1|" + id + "|" + fingerprint), in Crockford base32 (0123456789ABCDEFGHJKMNPQRSTVWXYZ), 32 characters in 8 groups of 4. For V1 the HMAC input is riftveil-seal-v1|XT-260101-000000-0000-0000-0000|abc5ba903a307668121b1fdd0e01ce784069189c1b255e481f65f5e54ee765fc.

6. Add verification to a page

<div data-riftveil-verify data-lang="en"></div>
<script src="https://riftveil.ai/embed/verify.js" defer></script>

The host page's Content-Security-Policy must allow script-src and connect-src https://riftveil.ai.

7. Become an issuer

Register a prefix in ISSUERS (prefix, name, contact, verification endpoint, address of the key list), publish your fingerprint profiles if you add any, create your signing key yourself and keep it, like your sealing key, in a secret store, publish your key list, and follow the issuer rules of Conformance.