{"revision":"0ed62d2c742f8e20","language":"en","entry":{"id":"verify-file","kind":"endpoint","group":"keys","title":"Verify a file or a release","method":"POST","path":"/v1/verify","paths":[],"summary":"Check the signature of a file from its hash, or of a signed checksum list, against a card’s keys. This POST only reads.","response":"The verdict of a message’s verification, plus format, external, signedAt, atFrom, covers, hash, file, list and match.","parameters":[],"sample":"/v1/verify","access":"public","base":"directory","notes":["The server never sees the file. Send its hash, computed on your side, with the signature. A signature over the file’s bytes (an OpenPGP detached signature, a legacy minisign signature) answers 422 file_needed: check it in a browser on veil.rip/verify or with the GitHub action, or sign a checksum list.","File mode starts with sha256, sha512, blake2b512 or list, or with a signature that comes without key and message. It does not mix with key, message, platform, subject or block.","An OpenPGP signature carries a date that the signer’s own clock wrote, and the key’s state is read at that date unless you send at. Pass a trusted publication time in at when you have one. SSH and minisign signatures carry no date."],"errors":["400 hash_needed","400 invalid_block","400 invalid_file_signature","400 invalid_json","400 invalid_parameter","401 invalid_api_key","403 api_key_scope","403 api_key_suspended","403 developer_suspended","413 payload_too_large","422 file_needed","429 rate_limited","500 internal_error","503 busy"],"related":["verify-files","verify-message","external-keys-file","lookup"],"sections":[{"kind":"code","language":"shell","code":"sha512=$(sha512sum veil-1.2.tar.gz | cut -d' ' -f1)\njq -n --rawfile sig veil-1.2.tar.gz.sig --arg h \"$sha512\" \\\n  '{signature: $sig, sha512: $h, name: \"veil-1.2.tar.gz\"}' \\\n  | curl -fsS https://veil.rip/v1/verify --json @-"},{"kind":"code","language":"shell","code":"jq -n --rawfile sig SHA256SUMS.asc --rawfile list SHA256SUMS \\\n  --arg h \"$(sha256sum veil-1.2.tar.gz | cut -d' ' -f1)\" \\\n  '{signature: $sig, list: $list, sha256: $h, name: \"veil-1.2.tar.gz\", card: \"1\"}' \\\n  | curl -fsS https://veil.rip/v1/verify --json @-"}],"verify":"This is the server’s verdict over the hash you sent. To check it yourself, verify the signature over the file with the card’s keys from GET /u/{card}/keys.txt, and the card’s proofs in a browser.","bodyFields":[{"name":"signature","description":"The signature as the tool wrote it: the .sig of ssh-keygen -Y sign -n file, the .asc of gpg (or its binary .sig in base64), a gpg --clear-sign text, a .minisig file, or a Veil block.","in":"body","required":true},{"name":"sha256, sha512, blake2b512","description":"The file’s hashes in hex, computed by you. Send the one the signature uses: an SSH signature names it, a prehashed minisign signature uses BLAKE2b-512, a Veil block of a file names the SHA-256.","in":"body","required":false},{"name":"name","description":"The file’s name, at most 255 bytes. In a list, the line with this name is used, else the one with this name without folders.","in":"body","required":false},{"name":"list","description":"The exact text of the checksum list a detached signature covers (SHA256SUMS or SHA512SUMS, at most 1000 lines). A clear-signed list and a release’s Veil block carry their own.","in":"body","required":false},{"name":"format","description":"ssh, pgp, minisign or veil. Without it, the server reads it from the text.","in":"body","required":false},{"name":"card","description":"A card’s number or alias. Needed for an OpenPGP signature made by a signing subkey, and for a key the card has removed.","in":"body","required":false},{"name":"at","description":"Optional Unix seconds. Defaults to the date of an OpenPGP signature, else now.","in":"body","required":false}],"live":false,"responses":[{"status":200,"contentType":"application/json","description":"valid is the overall verdict: the signature holds, the key was in force, no staff ban stops it, and the file’s hash is the signed one. The other fields explain it.","fields":[{"name":"valid","type":"boolean","description":"Combined verdict for signature, key state and any requested account ownership. HTTP 200 also covers `valid=false`."},{"name":"signatureOk","type":"boolean","description":"Whether the cryptographic signature verifies."},{"name":"identity","type":"IdentityRef | null","description":"Card associated with the key or named in the block, when known."},{"name":"device","type":"string | null","description":"Signing device, when identified."},{"name":"role","type":"string | null","description":"Signing role, when identified."},{"name":"keyState","type":"KeyState | null","description":"Key validity at the requested time."},{"name":"account","type":"object | null","description":"platform, subject, owned and reason when platform/subject were requested."},{"name":"reason","type":"string | null","description":"Null when valid. Otherwise bad_signature, unknown_key, unknown_identity, other_card, not_yet_valid, revoked, key_banned, identity_banned, identity_erased, hash_mismatch or not_in_list."},{"name":"key","type":"string | null","description":"The device key in hex for a Veil block, the public key from elsewhere as the card writes it for the other formats."},{"name":"format","type":"string","description":"ssh, pgp, minisign or veil."},{"name":"external","type":"object | null","description":"The public key from elsewhere: type, fp, label, addedIn, removedIn, and signer, the key that signed (an OpenPGP signing subkey’s fingerprint when a subkey signed)."},{"name":"signedAt","type":"string | null","description":"The date an OpenPGP signature states, written by the signer’s clock. Null for the other formats."},{"name":"atFrom","type":"string","description":"Where the time of the check comes from: request, signature or now."},{"name":"covers","type":"string","description":"file or list."},{"name":"hash","type":"string | null","description":"The file’s hash the check used: sha256, sha512 or blake2b512."},{"name":"file","type":"object | null","description":"The name and hashes you sent."},{"name":"list","type":"object | null","description":"The signed list’s lines: name, alg and hash."},{"name":"match","type":"object | null","description":"The line of the block or list for your file: name, alg, expected and ok."}],"example":{"language":"json","code":"{\n  \"valid\": false,\n  \"signatureOk\": true,\n  \"identity\": {\n    \"n\": 1,\n    \"url\": \"https://veil.rip/u/1\",\n    \"name\": \"Marta Lanzoni\",\n    \"nameHidden\": false\n  },\n  \"device\": null,\n  \"role\": null,\n  \"keyState\": {\n    \"at\": 1793287911,\n    \"active\": true,\n    \"reason\": \"active\",\n    \"validFrom\": \"2026-10-29T15:54:50Z\",\n    \"validUntil\": null\n  },\n  \"account\": null,\n  \"reason\": \"hash_mismatch\",\n  \"key\": \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKX/sjzOxM0MkL47LkMqAm0iZXcdiRGBJiqvesABK66k\",\n  \"format\": \"ssh\",\n  \"external\": {\n    \"type\": \"ssh-ed25519\",\n    \"fp\": \"SHA256:yKGK3rJbLA9EAbnKwNXuQvLvsvT0WRrii16h6sF9Rwk\",\n    \"label\": \"Release key\",\n    \"addedIn\": 2,\n    \"removedIn\": null,\n    \"signer\": \"SHA256:yKGK3rJbLA9EAbnKwNXuQvLvsvT0WRrii16h6sF9Rwk\"\n  },\n  \"signedAt\": null,\n  \"atFrom\": \"now\",\n  \"covers\": \"list\",\n  \"hash\": \"sha256\",\n  \"file\": {\n    \"name\": \"veil-1.2.tar.gz\",\n    \"sha256\": \"5d2aebc96df7ba9f758fbaaca850d629e39284b55d676c828e6498962b694d12\",\n    \"sha512\": null\n  },\n  \"list\": {\n    \"lines\": [\n      {\n        \"name\": \"veil-1.2.tar.gz\",\n        \"alg\": \"sha256\",\n        \"hash\": \"04cac27551ecc1b787e5ce856537f28802f70c1222efa5454f0ab626f4c242c2\"\n      }\n    ]\n  },\n  \"match\": {\n    \"name\": \"veil-1.2.tar.gz\",\n    \"alg\": \"sha256\",\n    \"expected\": \"04cac27551ecc1b787e5ce856537f28802f70c1222efa5454f0ab626f4c242c2\",\n    \"ok\": false\n  }\n}"}}],"responseObjects":[{"id":"key-ref","title":"Identity reference","fields":[{"name":"n","type":"integer","description":"Permanent card number."},{"name":"url","type":"string","description":"Permanent card URL."},{"name":"name","type":"string","description":"Current display name."},{"name":"nameHidden","type":"boolean","description":"Whether staff hide the name."}],"typeName":"IdentityRef"},{"id":"key-state","title":"Key state at a time","fields":[{"name":"at","type":"integer","description":"Time checked, in Unix seconds."},{"name":"active","type":"boolean","description":"Whether the key was active at that time."},{"name":"reason","type":"string","description":"active, not_yet_valid or revoked."},{"name":"validFrom","type":"string","description":"Publication time of the version that added it."},{"name":"validUntil","type":"string | null","description":"Publication time of revocation, or null."}],"typeName":"KeyState"}],"errorFormat":"json","errorDetails":[{"status":400,"code":"hash_needed","description":"The signature covers a hash of the file that the request does not carry. The message begins with its name: sha256, sha512 or blake2b512.","action":"Compute that hash of the file (sha256sum, sha512sum or b2sum) and send it in the field of the same name.","scope":"endpoint"},{"status":400,"code":"invalid_block","description":"The signature block cannot be parsed or names another directory.","action":"Paste the complete block with its message and use the directory it names.","scope":"endpoint"},{"status":400,"code":"invalid_file_signature","description":"The signature of a file or a checksum list can’t be read. The message begins with the reason: unreadable, namespace (an SSH signature made for another use than files), pgp_type, pgp_version, weak_hash, list_unreadable or block_message.","action":"Send the signature file as the tool wrote it. Sign files with ssh-keygen -Y sign -n file, gpg --detach-sign with SHA-256 or SHA-512, or minisign -Sm.","scope":"endpoint"},{"status":400,"code":"invalid_json","description":"The body is not a JSON object, contains duplicate keys or has trailing data.","action":"Send one valid JSON object with `Content-Type` application/json.","scope":"endpoint"},{"status":400,"code":"invalid_parameter","description":"A path, query or body field is missing, malformed or outside its accepted range.","action":"Correct the field named in `error.message` before sending again.","scope":"endpoint"},{"status":401,"code":"invalid_api_key","description":"The bearer credential is malformed, unknown, expired or revoked.","action":"Use an active key. To use anonymous public access, remove `Authorization` entirely. Invalid credentials never fall back automatically.","scope":"optional-api-key"},{"status":403,"code":"api_key_scope","description":"The API key lacks read or verify scope, or the endpoint does not accept bearer keys.","action":"Use the correct scope. Identity writes and key management still require a device signature.","scope":"optional-api-key"},{"status":403,"code":"api_key_suspended","description":"This API key is suspended.","action":"Ask staff to restore it; another key cannot lift an account suspension.","scope":"optional-api-key"},{"status":403,"code":"developer_suspended","description":"The owner’s API account is suspended or the owner card is unavailable.","action":"Check the owner account and any staff restriction. Creating another key does not bypass suspension.","scope":"optional-api-key"},{"status":413,"code":"payload_too_large","description":"The request body exceeds this endpoint’s byte limit.","action":"Reduce the encoded body. API-key management accepts at most 8 KiB.","scope":"endpoint"},{"status":422,"code":"file_needed","description":"An OpenPGP detached signature or a legacy minisign signature of the file itself. It covers the file’s bytes, and the server never sees the file.","action":"Check it where the file is: on veil.rip/verify in a browser or with the GitHub action. Or sign a checksum list, which the server checks in every format.","scope":"endpoint"},{"status":429,"code":"rate_limited","description":"An anonymous, signed-request or API-key allowance was exceeded.","action":"Wait at least `Retry-After` seconds. Share a queue and retry budget across your workers.","scope":"common"},{"status":500,"code":"internal_error","description":"An unexpected server error occurred. Internal details are not returned.","action":"Retry reads with bounded backoff. Check write results before sending a new signed operation.","scope":"common"},{"status":503,"code":"busy","description":"The application has reached a temporary concurrency or database limit.","action":"Wait `Retry-After` seconds and retry with bounded backoff.","scope":"common"}],"responseHeaders":[{"name":"Content-Type","value":"application/json","description":"Use the declared media type before choosing a parser."},{"name":"Date","value":"HTTP date","description":"Server time. Compare it with your clock when signing requests."},{"name":"Retry-After","value":"integer seconds, on rate_limited or busy","description":"Wait at least this long before retrying. Other 429/503 codes do not necessarily include it."}],"documentationUrl":"https://veil.rip/docs/verify-file","supportsBrowserFetch":true,"browserMode":"fetch"}}