# Commitments Some challenges are drawn as a secret program before the camera opens. At that moment we publish a SHA-256 digest of the program, and you receive it in the session create response. The program itself is released one step at a time while the check runs. Once the session has finished, you can open the commitment: we return the program and the random salt behind each digest, and you recompute the hash yourself. If it matches the digest you were given at the start, the challenge was fixed before your user was shown anything, and a recording made in advance could not have contained a response to it. You do not have to take our word for that. ## Where the digest comes from A challenge that commits carries `commitmentHash` in its `params` in the `challengeSequence` of the [session create response](/docs/sessions). Store it with the `sessionId` if you want to check the opening against a value you held before the session ran, rather than against the copy in the opening itself. Not every session draws a committing challenge. A session with none has nothing to open. ## Open a session's commitments ```bash curl https://machine.cognau.com/api/v1/cockpit/sessions/6a7de599a5244ea6a3e65109/commitments \ -H "Authorization: Bearer $COGNAU_SECRET_KEY" ``` Authenticate with your secret API key, from your server. Only sessions that belong to your account are returned; any other id is a `404`. ### When you can call it Only once the session can no longer run a challenge: after it has been scored, passed, failed, sent to review or expired. While it is still being created or is running, the call is refused, because opening a commitment early would hand over the answer to a challenge that has not been asked yet: ```json { "success": false, "error": "This session is still running. Commitments open once it has finished.", "code": "session_live" } ``` That is a `409`, not a `403`. You are entitled to the opening, just not yet. Retry after the `verification.completed` webhook arrives. ### Response ```json { "success": true, "data": { "sessionId": "6a7de599a5244ea6a3e65109", "status": "verified", "verification": { "algorithm": "sha256", "preimage": "JSON.stringify(program) + \":\" + salt" }, "commitments": [ { "type": "…", "index": 2, "commitmentHash": "2b0496dc12dffe41619a8885dba46976a9c2ffd57af3b56d514dbcce07ba2c25", "program": ["a", "b", "c"], "salt": "0f1e2d3c4b5a69788796a5b4c3d2e1f0", "matches": true } ] } } ``` | Field | Meaning | |---|---| | `verification` | How to recompute each digest. It travels with the data so the recipe cannot drift from it. | | `commitments` | One entry per challenge that committed. An empty list is an ordinary answer. | | `type`, `index` | Which challenge in the sequence the entry belongs to. | | `commitmentHash` | The digest published when the session was created. | | `program` | The secret program, revealed now that it can no longer be acted on. Treat it as opaque JSON. | | `salt` | The random salt the digest was taken over. | | `matches` | Our own recomputation of the digest, compared with `commitmentHash`. | `matches` is always `true` in normal operation. It is computed rather than assumed so that a stored opening that does not match shows up in the response instead of waiting for you to find it by hand. It is a convenience, not the proof: the proof is your own recomputation below. The opening returns only what your user was shown. It never includes how the response was scored. ## Recompute the digest The preimage is the program serialised as compact JSON, then a colon, then the salt. The program in the example above is a made-up placeholder; use whatever the response carries. **JavaScript (Node)** ```js import { createHash } from 'node:crypto'; function verifyOpening(c, digestYouStored = c.commitmentHash) { const digest = createHash('sha256') .update(JSON.stringify(c.program) + ':' + c.salt) .digest('hex'); return digest === digestYouStored; } // ["a","b","c"]:0f1e2d3c4b5a69788796a5b4c3d2e1f0 // -> 2b0496dc12dffe41619a8885dba46976a9c2ffd57af3b56d514dbcce07ba2c25 ``` **Python** ```python import hashlib, json def verify_opening(c, digest_you_stored=None): preimage = json.dumps(c["program"], separators=(",", ":"), ensure_ascii=False) + ":" + c["salt"] digest = hashlib.sha256(preimage.encode("utf-8")).hexdigest() return digest == (digest_you_stored or c["commitmentHash"]) ``` Two details make the Python match byte for byte: `separators=(",", ":")` removes the spaces `json.dumps` adds by default, and `ensure_ascii=False` keeps characters as they are instead of escaping them. Both languages keep object keys in the order they were parsed, so re-serialising the parsed `program` reproduces the original bytes.