Verifying a Conectir archive
For anyone who has been handed a Conectir case archive (a .zip made by “Download the complete archive” or “Close and archive this case”) and wants to know, from the zip alone, that it is the one Conectir produced and that nothing inside it has changed. No Conectir account is needed; a key for the verify API is, and Conectir support issues one on request.
Two checks, in this order. The first needs nothing but the zip. The second needs one call.
1. Every entry against the manifest
Section titled “1. Every entry against the manifest”Open the zip. manifest.json lists every entry except README.txt, manifest.json and seal.json, each with its SHA-256 fingerprint in lowercase hex, in zip order:
"entries": [ { "name": "case.json", "sha256": "…64 hex…", "bytes": 1234, "kind": "core" }, { "name": "tables/cases.json", "sha256": "…", "bytes": 567, "kind": "table", "table": "cases", "rows": 1 }, …]Compute the SHA-256 of each entry’s bytes and compare it with the listed fingerprint. Every entry must match, every listed name must be present, and nothing may be in the zip that is not listed (apart from the three files above). A difference means the archive changed after it was produced.
This proves the entries against the manifest. It does not yet prove the manifest.
2. The seal against Conectir
Section titled “2. The seal against Conectir”seal.json, the last file in the zip, holds:
{ "sealVersion": 1, "algorithm": "HMAC-SHA256", "keyId": "k-2026-10", "caseId": "11111111-1111-1111-1111-111111111111", "producedAt": "2026-10-23T00:00:00.000Z", "build": "v2.33.00", "entries": 412, "seal": "…64 hex…"}The seal is an HMAC-SHA256 under a key only Conectir holds. You do not compute it; you ask Conectir whether it is right for what you have.
Build the canonical string, exactly, from the manifest’s caseId, producedAt and entries in order. Each line ends with a newline (\n, byte 0x0A); the name and the fingerprint are separated by one tab (\t, byte 0x09); the whole thing is UTF-8:
conectir-archive-seal/1\ncaseId=<caseId>\nproducedAt=<producedAt>\n<entry name>\t<sha256>\n ← one line per entry, in manifest order(You do not send this string. The API builds it the same way from what you send, and hashes it. It is described so that what you send is unambiguous.)
Then call:
POST https://api.conectir.com/api/v1/archive/verifyAuthorization: Bearer cak_…Content-Type: application/json
{ "keyId": "k-2026-10", "caseId": "11111111-1111-1111-1111-111111111111", "producedAt": "2026-10-23T00:00:00.000Z", "entries": [ { "name": "case.json", "sha256": "…" }, { "name": "tables/cases.json", "sha256": "…" } ], "seal": "…64 hex…"}Use the fingerprints you computed in step 1, not the manifest’s, so that a manifest edited to match tampered bytes still fails here.
The answer:
| Answer | Meaning |
|---|---|
{ "valid": true } |
The seal is right for exactly these names and fingerprints under this key id. With step 1 passed, the archive is whole. |
{ "valid": false, "reason": "seal-mismatch" } |
The archive is not what Conectir sealed. Something changed, or this seal belongs to a different archive. |
{ "valid": false, "reason": "unknown-key" } |
No key with that id exists at this portal. Either the archive came from a different Conectir instance, or seal.json was altered. |
{ "valid": false, "reason": "malformed" } (400) |
The request shape. Check the UUID, the ISO time with milliseconds and Z, 64-hex fingerprints, no tab or newline in a name. |
{ "valid": false, "reason": "master-unavailable" } (503) |
Conectir cannot verify anything at this moment. Not a verdict on the archive; try later. |
{ "valid": false, "reason": "rate-limited" } (429) |
More than 60 calls a minute on this key. |
{ "valid": false, "reason": "unauthorized" } (401) |
No key, a revoked key, or a malformed one. |
Nothing about a call is kept beyond a count on the key: no case id, no entry name, no fingerprint and no verdict is logged or stored. Nothing from inside the archive is sent: names and hashes only. Up to 50,000 entries per call.
Worked example, with a test key
Section titled “Worked example, with a test key”These values were computed with a test master and are frozen in Conectir’s own tests; they will not verify at the real portal (its masters are different) but they let you check your canonical string and digest before you call.
caseId11111111-1111-1111-1111-111111111111,producedAt2026-10-23T00:00:00.000Z, two entries:case.jsonwith fingerprintaaaa…(64 ×a) andtables/cases.jsonwithbbbb…(64 ×b).- The canonical string is 5 lines: the prefix line, the two identity lines, and one line per entry.
- Its SHA-256 (the digest the portal signs) is
523d1b49ca581672dabb1e22ac0b39ffd5e3099d7993feed07730a8bc27e9e60. If your code gets that digest from those inputs, your canonical string is right. - Under the test master
test-master-0123456789abcdef0123456789abcdefand key idk-2026-10, the derived key isHKDF-SHA256(master, salt "conectir-archive-seal", info "k-2026-10", 32 bytes)and the seal is7a90b437229e1168c23b5396a01902fa53cc71cd159f33c2f8030e9736864c04. You never need the key; this is here so a Conectir engineer can check the portal’s side against the same vector.
What a seal does and does not prove
Section titled “What a seal does and does not prove”A valid seal proves the archive is byte for byte what Conectir produced at producedAt, and that its manifest was not edited since. It does not prove that any file inside was authentic when it entered Conectir; a fingerprint and a seal attest to bytes, not to truth. Conectir’s own custody and audit records, which the archive carries in tables/ and audit-log.json, are where provenance lives.
A key id names which of Conectir’s masters sealed the archive. Masters are never changed; a new one is only ever added beside the last, so an archive sealed years ago verifies for as long as the portal exists.