Verify a consent certificate
Check the AI terms recorded in a certificate, Sparkonomy's signature, its place in the public log, and any independent timestamp. Each result below answers a separate question.
What the certificate records
A consent certificate is a snapshot of a Creator's AI declaration, signed by Sparkonomy. It records the Creator's SPRK-ID, name, account references, and a separate stance for name, image, likeness, voice, movement, and signature. It also carries declaration identifiers and the recorded declaration time.
Read each facet separately:
- Allowed – records permission for AI training and generation of that facet
- Conditional – requires a licence
- Prohibited – records no permission for those uses
- No terms declared – grants no permission
The overall badge is a summary and cannot replace these individual terms.
The certificate is issued from the saved declaration event and its identity snapshot. Later profile edits do not rewrite that snapshot. A new declaration can produce a new certificate, while an older certificate can still have a valid signature.
Read the results separately
- Certificate signature and reference. The browser verifies the compact JWS with the published key named in its protected header. It then hashes the exact payload bytes and checks the certificate reference. Displayed terms come from this signed payload, never the unsigned, readable copy beside it.
- Current certificate. The browser compares this reference with the record's published certificate.json pointer, following registry redirects if the record has moved to another SPRK ID. This is the registry's current published answer, fetched separately from the signed snapshot. A match is not an independent proof that no newer declaration exists.
- Public log. The browser looks for this payload's digest in the named batch and rebuilds its Merkle root from the listed entries. It also checks the supplied inclusion proof and compares the batch's readable root and identifier with its JWS payload. These checks establish consistency with that batch; they do not audit the whole log or independently verify the batch's JWS signature.
- Independent timestamp. When a timestamp is available, the browser checks its message imprint against the batch checkpoint, verifies the authority's signature and certificate chain against the root pinned in this verifier, and checks certificate validity at the stamped time. It also checks the timestamp against the reported batch close and the browser's clock, allowing five minutes of clock skew.
A valid certificate signature can be shown while a log or timestamp check is still pending, unavailable, or fails. Read all the results before relying on the evidence. A network failure means a check could not be completed; it does not establish that a signature is invalid.
How the evidence fits together
The signed declaration
The issuer serialises the payload once as UTF-8 JSON with sorted keys and compact separators. The certificate reference is SPRKCG-C- followed by the first 32 hexadecimal characters of the payload's SHA-256 digest. The full digest is used in the log.
The signature uses ES256: ECDSA with P-256 and SHA-256 over the compact JWS signing input — the base64url-encoded protected header, a dot, and the base64url-encoded payload. It is not a signature over the printed reference or the pretty-printed JSON file. Changing a signed date or term changes the payload digest and invalidates the existing signature.
The batch and inclusion proof
The scheduled log task closes batches hourly, including empty batches. Each batch checkpoint contains its identifier, close time, entry count, Merkle root, and the previous batch's identifier and root. Sparkonomy signs that checkpoint and publishes the entries beside it.
A leaf is SHA-256(0x00 || payload_digest); an internal node is SHA-256(0x01 || left || right), using raw hash bytes. An unpaired node is carried to the next level unchanged. A certificate's anchor carries its leaf index, tree size, sibling hashes, root, and the addresses of its batch and timestamp evidence.
The anchor is added after the batch closes. It is outside the signed certificate payload, so adding it does not change the certificate reference. Its claims must be checked against the batch and timestamp. Timestamp evidence is never inside the signed payload.
The independent clock
The timestamping authority receives a SHA-256 imprint of the exact checkpoint bytes decoded from the batch's JWS payload. It stamps that checkpoint, not the JWS signature, the bare Merkle root, or each certificate individually. The RFC 3161 response is published separately as .tsr, alongside a readable .timestamp.json summary.
A verified timestamp, combined with the certificate's checked inclusion in that root, provides evidence that the signed payload existed no later than the authority's stamped time. It does not establish that the Creator declared it at the earlier time recorded by Sparkonomy, or when the terms took effect. A late timestamp establishes the later bound only.
Issuance and batch publication can proceed without a timestamp. Temporary authority failures are retried within configured limits; a missing token supplies no independent time evidence.
eIDAS-compliant timestamping
We use Sectigo's qualified timestamping service, which Sectigo identifies as compliant with eIDAS, the EU framework for electronic identification and trust services. It supplies the independent timestamp over a batch checkpoint. See Sectigo's timestamping documentation.
Under Article 41(2) of eIDAS, a qualified electronic timestamp benefits from a legal presumption that its date and time are accurate and the data bound to them retains its integrity. Qualification applies to the timestamping service and its issued timestamps; it does not turn Sparkonomy's consent certificate into a qualified electronic signature or certify the Creator's identity.
The browser checks the cryptographic evidence described above. It does not consult EU Trusted Lists or determine the service's qualified status at the time of issue. A batch without a verified timestamp has no verified eIDAS timestamp evidence on this page.
What you are trusting
The issuer. A valid signature establishes that the matching private key signed the payload. Sparkonomy publishes the keys and attests to the declaration. The Creator does not personally sign this JWS, and this check does not independently establish their identity, age, or ownership of every right mentioned.
The verifier and its trust anchors. The arithmetic runs locally, but this page, its JavaScript, and the issuer's keys are delivered by Sparkonomy. The browser verifier pins the Sectigo Qualified Time Stamping Root R45 in its source. It does not perform an online certificate-revocation check. For an independent audit, retain the evidence and confirm the issuer key and TSA trust anchor through a separately trusted source.
The history you can observe. Hashes and chained checkpoints make changes detectable when compared with retained or independently witnessed evidence. They do not prevent files from being removed, prove the log is complete, or prove that every reader was shown the same history. This page checks one certificate and its batch, not the entire chain.
The signed data. A shared certificate image can be edited. Compare it with the declaration verified here. A valid historical certificate does not by itself establish current permission; consult the current record and any applicable licence before using a facet.
Check the published files yourself
The Python verifier below downloads the certificate, published keys, and any batch and timestamp evidence its anchor names. It performs the checks locally. It needs Python 3.9 or newer, the cryptography, asn1crypto, and certifi packages, and OpenSSL 3 or newer with the ts command.
Install the dependencies with python3 -m pip install cryptography asn1crypto certifi. Save the code as verify_certificate.py and run python3 verify_certificate.py <reference>.
The script checks the payload reference, both certificate and batch signatures, the rebuilt Merkle root and inclusion proof, the independent timestamp, and the current-certificate pointer. It verifies the downloaded TSA root against a fingerprint pinned in its source, and takes the timestamp and policy from the signed token. OpenSSL verifies the token and certificate chain at the stamped time.
- 0 – all listed checks completed
- 1 – invalid evidence
- 2 – incomplete checks or a command-line usage error
(Missing evidence or a missing OpenSSL executable is reported as unchecked.)
A superseded certificate can still verify; read the separate current-status result.
The script uses the registry's published issuer keys and its mutable current pointer. Confirm the issuer keys separately for an independent audit. It checks one batch, not the full log chain, certificate revocation status, or eIDAS qualification against EU Trusted Lists.
Use the full SPRKCG-C-… reference from the certificate. The code below is the published verifier source.
"""Verify a published Open Creator Graph consent certificate without the repository.
Install: python3 -m pip install cryptography asn1crypto certifi
Also requires OpenSSL 3 or newer with the `ts` command.
Run: python3 verify_certificate.py SPRKCG-C-<32 lowercase hexadecimal characters>
Exit codes: 0 = all listed checks completed, 1 = invalid evidence, 2 = incomplete checks.
Issuer keys are fetched from the registry address the certificate names; confirm them
separately for an independent audit.
The TSA root is pinned below. This checks one batch, not the entire log, revocation status,
or eIDAS qualification against EU Trusted Lists. A superseded certificate can still verify.
"""
import argparse
import base64
import hashlib
import json
import re
import ssl
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path
import certifi
from asn1crypto import tsp
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
GRAPH = "https://creatorgraph.sparkonomy.com"
# Sectigo Qualified Time Stamping Root R45. Downloading its bytes does not decide trust:
# the DER fingerprint must equal this source-pinned value before OpenSSL receives it.
TSA_ROOT = "f871f8976b4068d700d5f281084b4a29eaf4b8f35743330ba062fab46f58c2ed"
FACETS = ("name", "image", "likeness", "voice", "movement", "signature")
HEAD_FIELDS = ("version", "registry", "batch", "closed_at", "leaf_count", "root", "prev_batch", "prev_root")
CLOCK_SKEW = timedelta(minutes=5)
class VerificationError(ValueError):
pass
def require(condition, message):
# Deliberately not assert: integrity checks must also run under python -O.
if not condition:
raise VerificationError(message)
def fetch(url):
require(isinstance(url, str) and url.startswith("https://"), "evidence URL must use HTTPS")
request = urllib.request.Request(url, headers={"Cache-Control": "no-cache"})
with urllib.request.urlopen(request, timeout=30, context=ssl.create_default_context(cafile=certifi.where())) as response:
require(response.url.startswith("https://"), "evidence redirected away from HTTPS")
return response.read()
def get(url):
return json.loads(fetch(url))
def key_set_url(doc):
"""Where the keys that signed this certificate are published.
Not a fixed address: the registry publishes one key set per environment, and a certificate
names its own, so the reader checks it against the key that actually signed it rather than
against whichever set happens to sit at the root.
Bounding it to the registry is the whole point of the check. A verifier that fetched
whatever address a document named would verify a forgery against the forger's own key set
and report it as signed by Sparkonomy. A certificate published before this field existed has
no `keys`, and the root is what it meant.
"""
named = doc.get("keys") or f"{GRAPH}/registry/keys.json"
require(
re.fullmatch(rf"{re.escape(GRAPH)}(?:/[a-z0-9][a-z0-9-]*)?/registry/keys\.json", named),
"certificate names a key set outside this registry",
)
return named
def optional(url, label, binary=False):
try:
return fetch(url) if binary else get(url)
except urllib.error.HTTPError as error:
if error.code != 404:
print(f"{label}: UNCHECKED (HTTP {error.code})")
else:
print(f"{label}: UNCHECKED (no evidence at the published address)")
except (urllib.error.URLError, TimeoutError, OSError) as error:
print(f"{label}: UNCHECKED (could not fetch evidence: {error})")
return None
def b64u(value):
require(isinstance(value, str) and re.fullmatch(r"[A-Za-z0-9_-]+", value), "invalid base64url segment")
return base64.b64decode(value + "=" * (-len(value) % 4), altchars=b"-_", validate=True)
def verify_jws(token, jwks):
parts = token.split(".")
require(len(parts) == 3, "a compact JWS must have three segments")
header_b64, payload_b64, signature_b64 = parts
header = json.loads(b64u(header_b64))
require(header.get("alg") == "ES256", "unsupported JWS algorithm (expected ES256)")
require("crit" not in header and "b64" not in header, "unsupported JWS header extension")
require(isinstance(header.get("kid"), str) and header["kid"], "missing signing key id")
keys = [key for key in jwks["keys"] if key.get("kid") == header["kid"]]
require(len(keys) == 1, "signing key is missing or ambiguous in the published key set")
jwk = keys[0]
require(jwk.get("kty") == "EC" and jwk.get("crv") == "P-256", "signing key must be EC P-256")
require(jwk.get("alg", "ES256") == "ES256" and jwk.get("use", "sig") == "sig", "key is not for ES256 signing")
require("key_ops" not in jwk or "verify" in jwk["key_ops"], "key does not permit verification")
x, y, raw = b64u(jwk["x"]), b64u(jwk["y"]), b64u(signature_b64)
require(len(x) == len(y) == 32 and len(raw) == 64, "invalid ES256 key or signature length")
key = ec.EllipticCurvePublicNumbers(int.from_bytes(x, "big"), int.from_bytes(y, "big"), ec.SECP256R1()).public_key()
der = encode_dss_signature(int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big"))
try:
key.verify(der, f"{header_b64}.{payload_b64}".encode("ascii"), ec.ECDSA(hashes.SHA256()))
except InvalidSignature as error:
raise VerificationError("JWS signature does not verify") from error
body = b64u(payload_b64)
return body, json.loads(body)
def digest_bytes(value):
require(isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value), "invalid SHA-256 digest")
return bytes.fromhex(value)
def leaf(digest):
return hashlib.sha256(b"\x00" + digest_bytes(digest)).digest()
def root_of(leaves):
nodes = list(leaves)
while len(nodes) > 1:
next_level = [hashlib.sha256(b"\x01" + nodes[i] + nodes[i + 1]).digest() for i in range(0, len(nodes) - 1, 2)]
if len(nodes) % 2:
next_level.append(nodes[-1])
nodes = next_level
return nodes[0] if nodes else hashlib.sha256(b"").digest()
def inclusion(anchor, digest):
index, size, path = anchor["leaf_index"], anchor["tree_size"], anchor["proof"]
require(type(size) is int and type(index) is int and 0 <= index < size, "invalid inclusion index or tree size")
require(isinstance(path, list), "inclusion proof must be a list")
expected = digest_bytes(anchor["root"])
node, at = leaf(digest), 0
while size > 1:
if index ^ 1 < size:
require(at < len(path), "inclusion proof is shorter than the tree")
sibling = digest_bytes(path[at])
at += 1
node = hashlib.sha256(b"\x01" + (sibling + node if index % 2 else node + sibling)).digest()
index //= 2
size = (size + 1) // 2
require(at == len(path), "inclusion proof has hashes to spare")
require(node == expected, "inclusion proof does not reach the anchored root")
def instant(value):
require(isinstance(value, str), "timestamp must be a string")
stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
require(stamp.tzinfo is not None, "timestamp must include a timezone")
return stamp
def check_batch(batch, anchor, digest, cert_id, jwks):
checkpoint, head = verify_jws(batch["jws"], jwks)
require(head["version"] == 1, "unsupported batch version")
for field in HEAD_FIELDS:
require(batch[field] == head[field], f"batch readable {field} disagrees with its signed checkpoint")
for field in ("batch", "root", "closed_at"):
require(anchor[field] == head[field], f"anchor {field} disagrees with its signed checkpoint")
entries = batch["leaves"]
require(isinstance(entries, list), "batch leaves must be a list")
size = head["leaf_count"]
require(type(size) is int and size == len(entries) == anchor["tree_size"], "batch entry count does not match")
require(root_of([leaf(entry["digest"]) for entry in entries]) == digest_bytes(head["root"]), "batch Merkle root does not match")
inclusion(anchor, digest)
entry = entries[anchor["leaf_index"]]
require(entry["digest"] == digest and entry["certificate"] == cert_id, "certificate does not occupy its claimed leaf")
instant(head["closed_at"])
print(f"3. Batch signature, Merkle root and inclusion proof: OK ({head['batch']})")
return checkpoint, head
def timestamp(checkpoint, head, anchor):
stamp = anchor.get("timestamp") or {}
if not stamp.get("token_url"):
print("4. Timestamp: UNCHECKED (no timestamp address supplied)")
return False
token = optional(stamp["token_url"], "4. Timestamp", binary=True)
if token is None:
return False
# Parse only to obtain the imprint and time. OpenSSL must authenticate this same token
# before any of its claims are printed as verified; the JSON sidecar is never trusted.
reply = tsp.TimeStampResp.load(token, strict=True)
require(reply["status"]["status"].native in {"granted", "granted_with_mods"}, "TSA did not grant a timestamp")
content = reply["time_stamp_token"]
require(content["content_type"].native == "signed_data", "timestamp is not CMS SignedData")
signed = content["content"]
require(signed["encap_content_info"]["content_type"].native == "tst_info", "timestamp does not carry TSTInfo")
info = signed["encap_content_info"]["content"].parsed
imprint = info["message_imprint"]
digest = hashlib.sha256(checkpoint).digest()
require(imprint["hash_algorithm"]["algorithm"].native == "sha256", "timestamp imprint must use SHA-256")
require(imprint["hashed_message"].native == digest, "timestamp covers a different checkpoint")
when = info["gen_time"].native
require(when <= datetime.now(timezone.utc) + CLOCK_SKEW, "timestamp is in the future")
require(when >= instant(head["closed_at"]) - CLOCK_SKEW, "timestamp predates the batch checkpoint")
pem = optional(f"{GRAPH}/registry/tsa/{TSA_ROOT}.pem", "4. TSA root", binary=True)
if pem is None:
return False
roots = x509.load_pem_x509_certificates(pem)
require(len(roots) == 1 and roots[0].fingerprint(hashes.SHA256()).hex() == TSA_ROOT, "TSA root does not match the pinned fingerprint")
# Closed files work on Windows too. Explicit CAfile/CApath/CAstore keep the trust store
# limited to the pinned root. Chain validity is evaluated at the token's authenticated time.
with tempfile.TemporaryDirectory() as work:
folder = Path(work)
tsr, root = folder / "batch.tsr", folder / "root.pem"
tsr.write_bytes(token)
root.write_bytes(pem)
empty = folder / "empty"
empty.mkdir()
command = ["openssl", "ts", "-verify", "-in", str(tsr), "-digest", digest.hex(),
"-CAfile", str(root), "-CApath", str(empty), "-CAstore", str(root),
"-attime", str(int(when.timestamp()))]
try:
done = subprocess.run(command, capture_output=True, text=True, timeout=30)
except (FileNotFoundError, subprocess.TimeoutExpired) as error:
print(f"4. Timestamp: UNCHECKED (OpenSSL 3 with ts support is required: {error})")
return False
require(done.returncode == 0, f"timestamp signature/chain does not verify: {done.stderr.strip()}")
print(f"4. RFC 3161 timestamp and pinned TSA chain: OK ({when.isoformat()})")
print(f" Signed policy OID: {info['policy'].native}; eIDAS qualification is not determined by this script.")
print(" The included payload existed no later than this time; the earlier declaration time is issuer-recorded.")
return True
def current(payload, cert_id):
record = payload["record"]["sprk_id"]
seen = set()
for _ in range(16):
require(isinstance(record, str) and re.fullmatch(r"SPRK-[0-9]{8,}", record), "invalid record id")
require(record not in seen, "current pointer redirect cycle")
seen.add(record)
pointer = optional(f"{GRAPH}/{record}/certificate.json", "5. Current status")
if pointer is None:
return False
require(
isinstance(pointer, dict) and pointer.get("record") == record, "current pointer names a different record"
)
if "redirect" not in pointer:
break
require("certificate" not in pointer, "ambiguous current pointer")
record = pointer["redirect"]
else:
raise VerificationError("too many current pointer redirects")
reference = pointer.get("certificate")
require(
isinstance(reference, str) and re.fullmatch(r"SPRKCG-C-[0-9a-f]{32}", reference),
"invalid current certificate reference",
)
if record != payload["record"]["sprk_id"]:
print(f" Record moved to {record}; checked its current pointer.")
status = (
"this certificate" if pointer["certificate"] == cert_id else "a different certificate (this one is superseded)"
)
print(f"5. Current status: registry points to {status}.")
print(" This is the registry's mutable published answer, not an independently signed status proof.")
return True
def main(cert_id):
require(re.fullmatch(r"SPRKCG-C-[0-9a-f]{32}", cert_id), "invalid certificate reference")
doc = get(f"{GRAPH}/registry/certificate/{cert_id}.json")
jwks = get(key_set_url(doc))
body, payload = verify_jws(doc["jws"], jwks)
digest = hashlib.sha256(body).hexdigest()
require(cert_id == f"SPRKCG-C-{digest[:32]}", "reference does not match the signed payload")
require(payload["version"] == 1, "unsupported certificate version")
facets = payload["consent"]["facets"]
require(set(facets) == set(FACETS) and all(value in {"allowed", "conditional", "prohibited", "none"} for value in facets.values()), "invalid consent facets")
issued = instant(payload["timestamps"]["issued_at"])
print(f"1. Certificate reference: OK ({cert_id})")
print("2. Certificate ES256 signature: OK (against the registry's published key)")
print(f" {payload['record']['name']} ({payload['record']['sprk_id']})")
print(f" Issuer-recorded declaration time: {issued.isoformat()}")
for facet in FACETS:
print(f" {facet}: {facets[facet]}")
anchored = stamped = False
anchor = doc.get("anchor")
if anchor:
batch = optional(anchor["url"], "3. Batch")
if batch is not None:
checkpoint, head = check_batch(batch, anchor, digest, cert_id, jwks)
require(issued <= instant(head["closed_at"]) + CLOCK_SKEW, "declaration time is after its batch closed")
anchored = True
stamped = timestamp(checkpoint, head, anchor)
else:
print("3. Log inclusion: UNCHECKED (no batch anchor; batches are scheduled hourly)")
print("4. Timestamp: UNCHECKED (requires an authenticated batch)")
status = current(payload, cert_id)
complete = anchored and stamped and status
print("Checks complete." if complete else "Checks incomplete; UNCHECKED is not a successful verification.")
return 0 if complete else 2
def cli():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("reference", help="full SPRKCG-C- certificate reference")
args = parser.parse_args()
try:
return main(args.reference)
except (urllib.error.URLError, TimeoutError, OSError) as error:
print(f"UNCHECKED: could not obtain evidence: {error}", file=sys.stderr)
return 2
except (ValueError, KeyError, TypeError, IndexError, AttributeError) as error:
print(f"VERIFICATION FAILED: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(cli())
Certificate files are published at https://creatorgraph.sparkonomy.com/registry/certificate/<reference>.json; issuer keys at the address the certificate’s keys field names, which is always on https://creatorgraph.sparkonomy.com. Hash the decoded JWS payload bytes, not the surrounding JSON or an edited copy.