Documentation / Language reference
Language reference
Look up syntax, query meanings and the 25 built-in primitives. New to Verifpal? Start with the quickstart.
Examples below are syntax fragments; their inputs need declarations in a complete model. Query lines belong inside the final queries[ … ] block. Read the full manual for detailed semantics.
No matching entries. Try a shorter term or choose “Everything”.
Model structure
Attacker
attacker[active]
The first declaration selects active or passive. Both attackers observe network messages and derive values using the symbolic primitive rules. An active attacker can also intercept, replace and inject traffic.
Principals and prior knowledge
principal Alice[
knows private sk
knows public context
generates nonce
]
A principal block describes local operations. Later blocks with the same name continue that principal’s state. A knows private value is initially available only to principals that declare it; repeat the same declaration to model a shared secret. knows public makes a value known to every principal and the attacker. Prior knowledge stays shared across sessions.
Fresh values and leaks
principal Alice[
generates nonce
leaks sk
]
generates creates a fresh value, private until disclosed, with a distinct copy in each session. leaks discloses a value the principal already knows to the attacker. To model delayed compromise, put the leak in a later phase.
Names, assignments and outputs
k_send, k_receive = HKDF(nil, secret, context)
tag = MAC(k_send, HASH(message))
Names use ASCII letters, digits and underscores and are case-insensitive. Constants have a global namespace and cannot be reassigned. Assignments introduce new names and require a primitive expression on the right; x = y is invalid. Nested calls are allowed. Bind multiple outputs in order, using the counts specified below.
Each argument must be available to the principal. // starts a line comment; /* … */ encloses a block comment. There are no strings, general-purpose numeric data literals, loops, mutable variables or user-defined primitives.
nil and discarded outputs
_ = SIGNVERIF(pk, message, signature)?
nil is a built-in public constant, known to the attacker. Use it for an absent context or associated-data value; it is not a secret, fresh nonce or wildcard. _ discards the name of an output while still performing the computation and any check. Each underscore creates a separate anonymous output.
Messages and guards
Alice -> Bob: [pk], signature
Messages appear between principal blocks. The sender must know each named constant, and the recipient must not already know that field. Assign primitive expressions to names before sending them. ASCII -> and Unicode → arrows are accepted.
Brackets guard one transmitted value: the attacker can observe it but cannot replace the sender’s delivered copy. This assumes authentication provided by the deployment. A guarded relay can still forward an earlier attacker-substituted input; guarding a later hop does not authenticate the value’s original source.
Checked operations
_ = SIGNVERIF(pk, message, signature)?
A trailing ? halts the principal when a check fails. It is supported by ASSERT, SPLIT, AEAD_DEC, SIGNVERIF, RINGSIGNVERIF and KEM_DECAP. Place required checks before consuming the message.
DEC, PKE_DEC and UNBLIND cannot take this suffix. An unmatched unchecked call remains an unreduced symbolic term; it does not represent a real API returning that term as plaintext.
Phases and delayed compromise
phase[1]
principal Alice[
leaks sk
]
The model starts in phase 0. Explicit phases increase by one. A later leak can reveal earlier recorded ciphertexts, but cannot justify an attacker action before the leak. Phases do not erase keys, clear attacker knowledge or provide wall-clock timing.
Peer scenarios
scenarios[
Alice[gpeer = gb]
Alice[gpeer = gm]
]
Place the optional scenarios block immediately before queries. Each entry binds one principal’s prior-knowledge placeholders to declared values. Every principal and message is cloned per scenario, with a shared attacker, before session replication. This lets an honest-peer run interact with a run involving a corrupt peer.
A binding must name a constant declared with knows by that principal; it cannot bind a field sent over the network. Multiple bindings in one entry are comma-separated. Only the listed peer configurations are analyzed. Open the Lowe attack example.
Security queries
Confidentiality
confidentiality? secret
Contradicted when the attacker can obtain the value a reached principal’s copy resolves to in an explored execution. A substituted value can be attacker-known even while the original honest secret remains private. The qualifier [attacker-supplied value] distinguishes that case. Query the original secret when its disclosure is your concern, or use a precondition for an accepted key.
Authentication
authentication? Alice -> Bob: proof
Checks the origin and duplicate acceptance of a value Bob successfully uses. A contradiction means that the used value does not come from Alice’s unchanged delivery or a matching run, or that one sending is successfully used by two recipient runs.
The message must be received and used inside a primitive. Required checks determine successful use; an unrelated later halt does not automatically undo an earlier use. This is an injective use-origin test, not a claim of general agreement or protocol completion. [duplicate acceptance] identifies a replay, which need not involve a forgery.
Freshness
freshness? nonce_hash
Contradicted when a principal successfully uses the queried value and its resolved term contains no generated constant. This is a structural test: it does not establish recency or reject every replay. A replayed value can still contain an old run’s generated nonce. Use authentication queries and the protocol’s actual challenge checks to investigate duplicate acceptance.
Equivalence
equivalence? k_alice, k_bob
Compares two or more distinct named values after substitution and rewriting in a principal state that holds them all. It is contradicted if eligible values differ under the symbolic equations. Values not reached because of a failed check, and failed checked outputs, are excluded. A pass alone does not show that both parties completed. This compares terms, not observational equivalence between protocols.
Unlinkability
unlinkability? token1, token2
Searches for a link witness between observable values with an honest secret-dependent relationship. Witnesses include observed equality, a shared identifying check, reconstruction from a common secret origin, and recognition of a secret tied to the other value. Attacker-manufactured relationships are excluded.
Use two or more distinct named constants. A pass means none of the supported witnesses was found; it is not a proof of indistinguishability. The manual gives the precise observability and eligibility conditions.
Query preconditions
confidentiality? k_client[
precondition[Client -> Server: request]
]
An option on any query, not a sixth query type. It restricts contradictions to executions in which the named sender reaches that send. Use a real message sent after the required checks to state a property on acceptance.
The event is the send, not receipt. The property’s failure and the send must occur in the same execution. Several preconditions require every named send. Adding a precondition can remove contradictions; it does not strengthen the unconditional property.
Cryptographic primitives
ASSERT
_ = ASSERT(expected, received)?
- Inputs
- 2
- Outputs
- 1
- Checked
- Yes: ?
- Assumptions
- None
Compares terms for equality. Use the question mark to stop on a mismatch; discard the output when only the check matters.
CONCAT
packet = CONCAT(header, payload)
- Inputs
- 2–5
- Outputs
- 1
- Checked
- No
- Assumptions
- None
Builds a structured tuple. Anyone holding it can extract its components. Boundaries and nesting are preserved; this does not model ambiguous byte concatenation.
SPLIT
header, payload = SPLIT(packet)?
- Inputs
- 1
- Outputs
- 1–5
- Checked
- Yes: ?
- Assumptions
- None
Selects tuple components in order. The input must resolve to a suitable CONCAT; a checked call stops on malformed input. It does not flatten nested tuples.
HASH
digest = HASH(message, context)
- Inputs
- 1–5
- Outputs
- 1
- Checked
- No
- Assumptions
- weak
Produces a symbolic hash of its arguments, in order. Holding an ideal hash does not reveal its preimage. Repeating the same inputs produces the same term.
MAC
tag = MAC(key, message)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- forgeable
Computes a keyed authenticator. Check a received tag with ASSERT(MAC(key, message), tag)? using the expected key and message.
HKDF
k_send, k_receive = HKDF(salt, secret, context)
- Inputs
- 3
- Outputs
- 1–5
- Checked
- No
- Assumptions
- None
Derives distinct output positions from a salt, input key material and context, in that order. Identical calls produce identical outputs in the same positions; new names alone do not make new keys.
PUBKEY
pk = PUBKEY(sk)
- Inputs
- 1
- Outputs
- 1
- Checked
- No
- Assumptions
- weak
Derives a public key from a private value. It does not publish that key. Used for signatures, public-key encryption, Diffie–Hellman and KEMs. The input cannot already be a PUBKEY or DH_KEX term.
DH_KEX
shared = DH_KEX(peer_public, my_private)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- None
Combines the peer’s public key with your private value. DH_KEX(PUBKEY(a), b) equals DH_KEX(PUBKEY(b), a). The second argument cannot be a public key, and DH_KEX cannot be nested inside itself.
ENC
ciphertext = ENC(key, message)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- weak, malleable
A deterministic symbolic abstraction of symmetric encryption, without explicit nonce, padding or mode. Use the malleable assumption to grant the modeled plaintext-replacement capability.
DEC
message = DEC(key, ciphertext)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- None
Reduces DEC(key, ENC(key, message)) to message. It has no checked-failure branch and cannot take a question mark.
AEAD_ENC
ciphertext = AEAD_ENC(key, nonce, message, ad)
- Inputs
- 4
- Outputs
- 1
- Checked
- No
- Assumptions
- weak, forgeable
Authenticated encryption with associated data. Use a distinct generated nonce for every encryption under one key. Two distinct ciphertexts made with a reused key/nonce pair reveal their plaintexts and enable forgery under that pair. Associated data is opaque inside this symbolic term; transmit it separately if it should be observable.
AEAD_DEC
message = AEAD_DEC(key, nonce, ciphertext, ad)?
- Inputs
- 4
- Outputs
- 1
- Checked
- Yes: ?
- Assumptions
- None
Authenticated decryption. Key, nonce and associated data must match the encryption. Use the question mark when failure must stop the principal. Attacker decryption also requires the nonce, so send it if the real protocol exposes it.
PKE_ENC
ciphertext = PKE_ENC(recipient_pk, message)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- weak
Encrypts for a recipient public key; decryption needs the corresponding private value. Anyone holding the public key can encrypt, so this does not authenticate the sender. The symbolic call is deterministic and has no randomness argument.
PKE_DEC
message = PKE_DEC(sk, ciphertext)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- None
Reduces PKE_DEC(sk, PKE_ENC(PUBKEY(sk), message)) to message. It cannot take a question mark; an unmatched call remains unreduced.
SIGN
signature = SIGN(sk, message)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- forgeable
Signs a message using a private key. Verify against the corresponding PUBKEY(sk). Repeating the same inputs produces the same signature term.
SIGNVERIF
_ = SIGNVERIF(pk, message, signature)?
- Inputs
- 3
- Outputs
- 1
- Checked
- Yes: ?
- Assumptions
- None
Checks the signature against the given public key and message. On success it reduces to nil, not to the signed message. The question mark enforces rejection. Trust in the public key must be established separately.
RINGSIGN
signature = RINGSIGN(sk_a, pk_b, pk_c, message)
- Inputs
- 4
- Outputs
- 1
- Checked
- No
- Assumptions
- forgeable
Creates a three-member ring signature. The first argument is the actual signer’s private key; the next two are the other members’ public keys. Verification identifies membership of the ring without identifying which member signed.
RINGSIGNVERIF
_ = RINGSIGNVERIF(pk_a, pk_b, pk_c, message, signature)?
- Inputs
- 5
- Outputs
- 1
- Checked
- Yes: ?
- Assumptions
- None
Verifies with all three ring public keys. Their order may differ from the signing call, but the keys must correspond one-to-one to the signed ring. Add the question mark to stop on failure.
BLIND
blinded = BLIND(factor, message)
- Inputs
- 2
- Outputs
- 1
- Checked
- No
- Assumptions
- None
Blinds a message under a secret factor so that another principal can sign the blinded term without learning the message. Keep the factor secret.
UNBLIND
signature = UNBLIND(factor, message, blinded_signature)
- Inputs
- 3
- Outputs
- 1
- Checked
- No
- Assumptions
- None
Converts SIGN(sk, BLIND(factor, message)) to SIGN(sk, message). The blind signature is the third argument. Anyone holding the factor, message and blind signature can unblind it. This operation cannot be checked.
KEM_ENCAP
shared, ciphertext = KEM_ENCAP(recipient_pk, seed)
- Inputs
- 2
- Outputs
- 2
- Checked
- No
- Assumptions
- weak
Encapsulates to PUBKEY(sk) using an explicitly generated seed. Bind the shared secret first and ciphertext second. Reusing inputs repeats outputs. A KEM does not authenticate who encapsulated; authenticate the ciphertext separately when required.
KEM_DECAP
shared = KEM_DECAP(sk, ciphertext)?
- Inputs
- 2
- Outputs
- 1
- Checked
- Yes: ?
- Assumptions
- None
Recovers the shared secret with the corresponding private decapsulation key. A checked call stops on a mismatched ciphertext. This deterministic symbolic interface does not model ML-KEM’s implicit rejection or probabilistic behavior.
THRESHOLD_SPLIT
s1, s2, s3 = THRESHOLD_SPLIT[2](secret)
- Inputs
- 1
- Outputs
- 2–16
- Checked
- No
- Assumptions
- None
Splits a secret so any t distinct shares recover it and fewer reveal nothing. The required numeric bracket carries t, from 2 through the number of bound outputs. Shares can also be used with THRESHOLD_SIGN.
THRESHOLD_JOIN
secret = THRESHOLD_JOIN(s1, s3)
- Inputs
- 2–16
- Outputs
- 1
- Checked
- No
- Assumptions
- None
Combines at least t distinct shares of one split. Matching partial signatures combine into SIGN(secret, message), and public share keys combine into PUBKEY(secret). Too few pieces, repeated shares, or disagreeing signature contexts leave the call unreduced.
THRESHOLD_SIGN
partial = THRESHOLD_SIGN(share, nonce, commitments, message)
- Inputs
- 4
- Outputs
- 1
- Checked
- No
- Assumptions
- forgeable
Produces a FROST partial signature using one share and a fresh secret nonce. At least t partials over distinct shares with the same commitments and message combine through THRESHOLD_JOIN. Reusing a nonce under one share reveals that share.
Analysis and results
Declared weakening assumptions
ciphertext = AEAD_ENC[weak from phase 2](key, nonce, message, ad)
Bracket annotations grant a specific attacker capability. weak removes the primitive’s modeled confidentiality guarantee; forgeable permits construction without its secret argument; malleable allows an attacker holding an ENC ciphertext to replace its plaintext with a constructible value.
Only the capabilities listed on each primitive are accepted. Separate capabilities with commas; from phase N delays the immediately preceding capability. The assumption list is part of the result: Verifpal does not establish that the real primitive is broken. THRESHOLD_SPLIT[2] uses a numeric threshold instead.
Sessions and search limits
verifpal verify model.vp --sessions 3
The default is two concurrent sessions per principal; the command line accepts 1–16, within expansion limits. Generated values are distinct per session, while values declared with knows remain shared. Scenarios multiply those copies. The Workbench uses the default session count.
Read every passing verdict’s search envelope, including session and term-depth limits and any incomplete-search qualifier. An attack needing more sessions can be missed. --saturate compares successive counts until neighboring result codes agree; this is not a proof that higher counts cannot change the answer.
Reading results
A result code records query types in order: c confidentiality, a authentication, f freshness, e equivalence and u unlinkability. A 0 means no contradiction was found; a 1 means the query was contradicted.
A contradiction describes the model and Verifpal’s query predicate. A pass describes the explored search. Neither alone establishes the security of an implementation or an unbounded protocol. Preserve the version, model, assumptions, verdict qualifiers and search limits when sharing a finding.
The fixed symbolic theory omits byte lengths, timing, arbitrary algebra, implementation bugs and general mutable state. The full manual explains the limits of replay semantics and its relationship to conventional concurrent execution.
Attack traces and reports
verifpal verify model.vp --format html > report.html
Read constructions, substitutions, checks and the final query violation together. A #2 suffix denotes another session; @2 denotes another scenario. Output positions such as |1 are report notation, not model syntax.
Keep diagnostic notes such as not causally ordered or not a minimized witness attached to the trace. The command line can export a self-contained HTML report or a LaTeX report with --format tex. View an example HTML report.
Based on the Verifpal user manual and the verifier’s primitive specifications. Source code · Research paper