veil

Verify a file or a release

Check the signature of a file from its hash, or of a signed checksum list, against a card’s keys. This POST only reads.

POST/v1/verify
Access
Public; API key optional
Client
Browser fetch supported

JSON body#

NameLocationMeaning
signatureRequiredJSONThe 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.
sha256, sha512, blake2b512JSONThe 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.
nameJSONThe 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.
listJSONThe 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.
formatJSONssh, pgp, minisign or veil. Without it, the server reads it from the text.
cardJSONA card’s number or alias. Needed for an OpenPGP signature made by a signing subkey, and for a key the card has removed.
atJSONOptional Unix seconds. Defaults to the date of an OpenPGP signature, else now.

Request examples#

cURL
sha512=$(sha512sum veil-1.2.tar.gz | cut -d' ' -f1)
jq -n --rawfile sig veil-1.2.tar.gz.sig --arg h "$sha512" \
  '{signature: $sig, sha512: $h, name: "veil-1.2.tar.gz"}' \
  | curl -fsS https://veil.rip/v1/verify --json @-
cURL
jq -n --rawfile sig SHA256SUMS.asc --rawfile list SHA256SUMS \
  --arg h "$(sha256sum veil-1.2.tar.gz | cut -d' ' -f1)" \
  '{signature: $sig, list: $list, sha256: $h, name: "veil-1.2.tar.gz", card: "1"}' \
  | curl -fsS https://veil.rip/v1/verify --json @-

Response#

HTTP 200application/json

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.

FieldTypeMeaning
validbooleanCombined verdict for signature, key state and any requested account ownership. HTTP 200 also covers valid=false.
signatureOkbooleanWhether the cryptographic signature verifies.
identityIdentityRef | nullCard associated with the key or named in the block, when known.
devicestring | nullSigning device, when identified.
rolestring | nullSigning role, when identified.
keyStateKeyState | nullKey validity at the requested time.
accountobject | nullplatform, subject, owned and reason when platform/subject were requested.
reasonstring | nullNull 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.
keystring | nullThe device key in hex for a Veil block, the public key from elsewhere as the card writes it for the other formats.
formatstringssh, pgp, minisign or veil.
externalobject | nullThe 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).
signedAtstring | nullThe date an OpenPGP signature states, written by the signer’s clock. Null for the other formats.
atFromstringWhere the time of the check comes from: request, signature or now.
coversstringfile or list.
hashstring | nullThe file’s hash the check used: sha256, sha512 or blake2b512.
fileobject | nullThe name and hashes you sent.
listobject | nullThe signed list’s lines: name, alg and hash.
matchobject | nullThe line of the block or list for your file: name, alg, expected and ok.

Example response

Illustrative values. Read the current response from your request.

json
{
  "valid": false,
  "signatureOk": true,
  "identity": {
    "n": 1,
    "url": "https://veil.rip/u/1",
    "name": "Marta Lanzoni",
    "nameHidden": false
  },
  "device": null,
  "role": null,
  "keyState": {
    "at": 1793287911,
    "active": true,
    "reason": "active",
    "validFrom": "2026-10-29T15:54:50Z",
    "validUntil": null
  },
  "account": null,
  "reason": "hash_mismatch",
  "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKX/sjzOxM0MkL47LkMqAm0iZXcdiRGBJiqvesABK66k",
  "format": "ssh",
  "external": {
    "type": "ssh-ed25519",
    "fp": "SHA256:yKGK3rJbLA9EAbnKwNXuQvLvsvT0WRrii16h6sF9Rwk",
    "label": "Release key",
    "addedIn": 2,
    "removedIn": null,
    "signer": "SHA256:yKGK3rJbLA9EAbnKwNXuQvLvsvT0WRrii16h6sF9Rwk"
  },
  "signedAt": null,
  "atFrom": "now",
  "covers": "list",
  "hash": "sha256",
  "file": {
    "name": "veil-1.2.tar.gz",
    "sha256": "5d2aebc96df7ba9f758fbaaca850d629e39284b55d676c828e6498962b694d12",
    "sha512": null
  },
  "list": {
    "lines": [
      {
        "name": "veil-1.2.tar.gz",
        "alg": "sha256",
        "hash": "04cac27551ecc1b787e5ce856537f28802f70c1222efa5454f0ab626f4c242c2"
      }
    ]
  },
  "match": {
    "name": "veil-1.2.tar.gz",
    "alg": "sha256",
    "expected": "04cac27551ecc1b787e5ce856537f28802f70c1222efa5454f0ab626f4c242c2",
    "ok": false
  }
}

Expand a nested object for its fields. Types ending in null allow a null value; optional fields may be absent. Other fields are present in that object.

Identity reference IdentityRef
FieldTypeMeaning
nintegerPermanent card number.
urlstringPermanent card URL.
namestringCurrent display name.
nameHiddenbooleanWhether staff hide the name.
Key state at a time KeyState
FieldTypeMeaning
atintegerTime checked, in Unix seconds.
activebooleanWhether the key was active at that time.
reasonstringactive, not_yet_valid or revoked.
validFromstringPublication time of the version that added it.
validUntilstring | nullPublication time of revocation, or null.

Response headers#

HeaderValueMeaning
Content-Typeapplication/jsonUse the declared media type before choosing a parser.
DateHTTP dateServer time. Compare it with your clock when signing requests.
Retry-Afterinteger seconds, on rate_limited or busyWait at least this long before retrying. Other 429/503 codes do not necessarily include it.

What to 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.

Usage 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#

API errors return an error object. Use error.code for program logic; error.message is English text for people and can change. Response and error guide

HTTPCodeMeaning and next step
400hash_needed

The signature covers a hash of the file that the request does not carry. The message begins with its name: sha256, sha512 or blake2b512.

Compute that hash of the file (sha256sum, sha512sum or b2sum) and send it in the field of the same name.

400invalid_block

The signature block cannot be parsed or names another directory.

Paste the complete block with its message and use the directory it names.

400invalid_file_signature

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.

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.

400invalid_json

The body is not a JSON object, contains duplicate keys or has trailing data.

Send one valid JSON object with Content-Type application/json.

400invalid_parameter

A path, query or body field is missing, malformed or outside its accepted range.

Correct the field named in error.message before sending again.

413payload_too_large

The request body exceeds this endpoint’s byte limit.

Reduce the encoded body. API-key management accepts at most 8 KiB.

422file_needed

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.

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.

When using an optional API key
HTTPCodeMeaning and next step
401invalid_api_key

The bearer credential is malformed, unknown, expired or revoked.

Use an active key. To use anonymous public access, remove Authorization entirely. Invalid credentials never fall back automatically.

403api_key_scope

The API key lacks read or verify scope, or the endpoint does not accept bearer keys.

Use the correct scope. Identity writes and key management still require a device signature.

403api_key_suspended

This API key is suspended.

Ask staff to restore it; another key cannot lift an account suspension.

403developer_suspended

The owner’s API account is suspended or the owner card is unavailable.

Check the owner account and any staff restriction. Creating another key does not bypass suspension.

Shared request errors
HTTPCodeMeaning and next step
429rate_limited

An anonymous, signed-request or API-key allowance was exceeded.

Wait at least Retry-After seconds. Share a queue and retry budget across your workers.

500internal_error

An unexpected server error occurred. Internal details are not returned.

Retry reads with bounded backoff. Check write results before sending a new signed operation.

503busy

The application has reached a temporary concurrency or database limit.

Wait Retry-After seconds and retry with bounded backoff.