Fingerprint Spec

When the Asqav SDK signs an action, it first builds a small fingerprint of the action data. This page is the public-facing summary of that format, so any verifier can rebuild it independently and check our signatures.

The full byte-level spec, with worked examples and edge cases, lives at docs/fingerprint-spec.md in the SDK repo. The same spec is committed in the backend repo and tested on every change.

The format

The fingerprint is RFC 8785 (the JSON format spec, also known as JCS) applied to the {action_type, context} pair, then SHA-256 over the resulting bytes. Stdlib Python, with member names ordered by UTF-16 code unit as RFC 8785 section 3.2.3 requires:

python
import hashlib, json

def _utf16_ordered(value):
    if isinstance(value, dict):
        return {k: _utf16_ordered(value[k])
                for k in sorted(value, key=lambda k: k.encode("utf-16-be"))}
    if isinstance(value, list):
        return [_utf16_ordered(v) for v in value]
    return value

def fingerprint(action_type, context):
    payload = {"action_type": action_type, "context": context or {}}
    body = json.dumps(_utf16_ordered(payload), separators=(",", ":"),
                      ensure_ascii=False, allow_nan=False).encode("utf-8")
    return "sha256:" + hashlib.sha256(body).hexdigest()

The key order is the one place a plain json.dumps(sort_keys=True) goes wrong: it sorts by code point, which agrees with RFC 8785 across the Basic Multilingual Plane and diverges for any key containing a character above U+FFFF. The keys (U+FF20) and 😀 (U+1F600) canonicalize to {"😀":1,"@":1} with SHA-256 425159f5c1f0575fbcbf9d05a8f60cde3d040eae5166aa2136657564048651b6; the code-point order {"@":1,"😀":1} hashes to 1c314559129cce00bc1b3caa2ee37fa3e81f926aee65b34f8e4e21856b2de83b and is not conformant. This is conformance vector asqav-24-jcs-astral-key-order.

Output is sha256:<64 hex chars>. You send that as the hash field on /sign. Asqav signs it with the agent's ML-DSA key and returns a signature.

Cross-language guarantee

The Python SDK (asqav) and the TypeScript SDK (@asqav/sdk) produce byte-identical output for the same input. We ship conformance vectors at tests/conformance/vectors.json. CI on both SDKs runs them on every change, so the two stay in lockstep. If you write your own verifier in another language, run it against those vectors too.

Why it matters

Audits happen months later. If two implementations disagree on the bytes for the same input, signatures stop verifying and the audit trail loses its evidentiary value. The spec keeps the SDK, the backend, and any third-party verifier in lockstep.

Hash-only mode

By default on cloud, the SDK hashes the context locally and sends only the resulting sha256:<hex> value plus a small whitelisted metadata bag. The cloud never receives the context itself. That hash is unsalted unless the signer set an org_salt, so anyone holding the hash can test candidate contexts against it until one matches. See Privacy and Data handling for how this maps to GDPR Art 25 data minimization.

Full spec

The full byte-level spec is at github.com/jagmarques/asqav-sdk/blob/main/docs/fingerprint-spec.md. It covers the subtleties (float precision, Unicode normalization, tuples, datetime conversion, NaN/Infinity rejection) that any third-party implementer needs.

Related