Operation hash
Every request carries an operation_hash. It’s a fingerprint of what the operators approved: the action and its params. Outis computes it when the request is created and never changes it. Your executor computes it again from the operation it’s about to run and refuses when the two don’t match.
Why your executor checks it
Section titled “Why your executor checks it”An approval is for one operation. The operators saw a deploy of acme/payments-api at 8d93f71 and turned their keys for that. Between the approval and the run there’s a gap. A queued job can pick up newer params in it, or a bug can hand one request’s approval to the job for another.
So the executor hashes what it’s about to do and compares that with the operation_hash on the decision. If they match, it’s running what the operators saw. If they don’t, the approval was for something else and the executor stops, which means an approval for operation A can’t be spent on operation B.
Check the hash as well as the outcome: act only when outcome is authorized and the hash matches.
What it covers
Section titled “What it covers”The action id and the params. It leaves out who asked, when, the summary, the callback URL and the organization, so two requests for the same deploy have the same hash. The request id is what tells them apart.
The preimage
Section titled “The preimage”The hash is SHA-256 over these bytes, in this order:
- The domain string
outis.operation.v1followed by one zero byte. - The action id.
- For each param, sorted by key in bytewise order of the UTF-8 key: the key, then the value.
Every string in steps 2 and 3 is written as its UTF-8 byte length as a 4-byte big-endian unsigned integer, then its UTF-8 bytes. The domain string in step 1 has no length prefix.
The result is sha256: followed by the 64 lowercase hex digits of the digest. No params hashes the domain and the action alone.
Params are strings on the wire, so there’s nothing to normalize. Hash exactly the strings you sent: "2500.00" and "2500" are different operations.
Compute it
Section titled “Compute it”import hashlib
import struct
def operation_hash(action: str, params: dict[str, str]) -> str:
h = hashlib.sha256(b"outis.operation.v1\x00")
def write(s: str) -> None:
b = s.encode("utf-8")
h.update(struct.pack(">I", len(b)))
h.update(b)
write(action)
for key in sorted(params, key=lambda k: k.encode("utf-8")):
write(key)
write(params[key])
return "sha256:" + h.hexdigest()
import { createHash } from "node:crypto";
export function operationHash(action, params = {}) {
const h = createHash("sha256").update("outis.operation.v1\0");
const write = (s) => {
const b = Buffer.from(s, "utf8");
const n = Buffer.alloc(4);
n.writeUInt32BE(b.length);
h.update(n).update(b);
};
write(action);
const keys = Object.keys(params).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
for (const key of keys) {
write(key);
write(params[key]);
}
return "sha256:" + h.digest("hex");
}
import sdk "github.com/outis-auth/outis-sdk"
hash := sdk.OperationHash("deploy.production", map[string]string{
"repo": "acme/payments-api",
"env": "production",
"sha": "8d93f71",
})
Test vectors
Section titled “Test vectors”Pin these in your own tests. An implementation that gets all three right handles the length prefix, the key order and multibyte UTF-8.
| Action | Params | Hash |
|---|---|---|
deploy.production |
none | sha256:da61379e121b0a00280eba6ee7d631e3c0c186e7da15ade5b8d15c901d0df8a9 |
deploy.production |
repo = acme/payments-api, env = production, sha = 8d93f71 |
sha256:54feb247e0ae56c01d430beb1b1c4c604384ca91832a1ccb435fe2753fb2be9e |
treasury.transfer |
amount = 250000.00, currency = USD, note = héllo, wörld |
sha256:02f4614a8117cdcd0407e0d085a6fa77ceb3814a0c68ab23ce416761bf64049e |
Where you’ll see it
Section titled “Where you’ll see it”- On the request object, from
POST /v1/requests,GET /v1/requests/{id}and every callback. - In the audit log, on the entry for the proposal, beside a hash of the policy the request was frozen under.
- Behind
Idempotency-Key: a replay is checked against the original’s hash, and reusing a key for a different operation is a 409.