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:
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.
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
- Governance Attestation uses the same JSON format for the attestation body.
- API Reference documents the
/signendpoint and both modes. - Data handling explains hash-only vs full-payload from the data-flow side.