Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix A — Error Registry

This appendix is the canonical registry of every error that a conforming Konstruct client can observe, organised by the layer that produces it. Each entry cites the exact variant in construct-core (or, where noted, the sibling construct-veil crate) and records the condition that raises it.

Keywords MUST, MUST NOT, SHOULD, MAY are per RFC 2119.

A.1 Layered error model

Errors in Konstruct travel up a stack of progressively narrower surfaces. An error condition that originates deep inside the cryptographic core is converted at each layer boundary into a variant appropriate for the consumer at that layer:

   application (iOS / macOS / Android UI)
                  ▲
                  │   uniffi_bindings::CryptoError      (FFI surface, §A.2)
   ────────────── ┼ ───────────────────────────────────────────────
                  │   cfe::CfeError                     (envelope,  §A.3)
                  │   wire_payload::WirePayloadError    (framing,   §A.4)
                  │   traffic_protection::PaddingError  (padding,   §A.5)
                  │
                  │   error::CryptoError                (internal,  §A.6)
                  │   utils::ConstructError             (top-level, §A.7)
                  │   group::mls_error::MlsError        (groups,    §A.8)
                  ▲
   Rust core

The dividing line at "FFI surface" is normatively significant: only the variants in §A.2 cross the UniFFI / JNI boundary by name. All deeper error types are mapped into SessionInitializationFailed, EncryptionFailed, or DecryptionFailed with a human-readable message field. Application code MUST NOT attempt to parse those message strings — they are diagnostic only.

The transport tier (Chapter 6 §6.5) has its own error surface in the separate construct-veil crate; it is included as §A.9 for completeness but is informational rather than normative for the core protocol.

Wire-level errors (server → client) flow as standard gRPC status codes and are covered in §A.10.

A.1.1 Stability policy

SurfaceStability
§A.2 FFI surfaceStable. Variants MUST NOT be reordered or repurposed. New variants are MINOR additions.
§A.3 CFE envelopeStable. The wire format is fixed; new validation errors are MINOR additions.
§A.4 WirePayloadStable. Header layout is normative.
§A.5 PaddingStable.
§A.6–A.8 InternalUnstable. Variant names and counts MAY change between releases. Catch the FFI surface (§A.2) instead.
§A.9 VEILUnstable. Tracked in the construct-veil crate.
§A.12–A.18Added in this revision (construct-core up to 0.48.1). Not covered by the §A.2 rule: variants MAY change between releases, and a client reads the UDL of the core it links.
§A.10 gRPCInherited from gRPC; semantics MUST follow the gRPC spec.

A.2 FFI surface — CryptoError

The application-facing error type exposed across UniFFI / JNI. Defined in construct-core/src/uniffi_bindings.rs:31.

TagVariantRaised whenRecovery
FFI-INITInitializationFailedThe core failed to initialise (e.g. RNG unavailable, keychain locked).Surface to user; retry after unlock or restart.
FFI-SESSION-NOT-FOUNDSessionNotFoundAn operation referenced a session id that no session exists for in local state.Trigger a fresh init_session for the peer.
FFI-SESSION-INITSessionInitializationFailed { message }X3DH / PQXDH handshake construction failed. Wraps a deeper error::CryptoError.Surface and retry after fetching a fresh prekey bundle.
FFI-ENCRYPTEncryptionFailed { message }RatchetEncrypt failed. Most commonly: session state corruption or AEAD allocation failure.Surface; do not silently fall back.
FFI-DECRYPTDecryptionFailed { message }RatchetDecrypt failed. Most commonly: bad AD, replayed message, skipped-key cache exhausted, or tampered ciphertext.Handled by the core, not the caller: the other held states are tried, a frame carrying the handshake header opens a new state, and otherwise a DECRYPTION_ERROR goes to the writer (Chapter 5 §5.9–§5.10). There is no heal and no END_SESSION.
FFI-INVALID-KEYInvalidKeyDataA key field had the wrong length or failed point validation.Reject the message / bundle.
FFI-INVALID-CTInvalidCiphertextA KEM ciphertext or AEAD frame failed structural validation.Reject the message.
FFI-SERIALIZESerializationFailedA MessagePack encode failed for an outbound CFE payload.Surface as internal error.
FFI-MSGPACK-DESERIALIZEMessagePackDeserializationFailedA MessagePack decode failed on an incoming CFE payload.Reject the envelope.
FFI-SPK-STALEPeerSpkStale { age_secs }The peer's Signed Pre-Key exceeds the clean-path SPK_MAX_AGE_SECS boundary (30 days in construct-core/src/crypto/client_api.rs:61-:71).Trigger a fresh bundle fetch / rotation wake. An implementation may offer an explicit degraded init_session_allowing_stale path, but it must still verify signatures and mark the resulting session at-risk.

FFI-SPK-STALE is the only variant with structured data that applications MUST react to: it conveys the age_secs so that UI can report "peer hasn't been online for N days" without re-deriving the threshold.

A.2.1 Session refusal prefixes

Session-level refusals travel inside the message of SessionInitializationFailed or as the code of a NotifyError action. Clients branch on the prefix, which is stable; the text after it is not.

Prefix / codeRaised whenRequired handling
PQ_REQUIREDThe bundle yields no Kyber prekey the core can trust — any PqxdhRefusal (Chapter 4 §4.3).No session. Do not retry classically; there is no classical path. A held state, if any, is left as it was.
PQ_DOWNGRADE_REFUSEDRemoved with PQXDH v2 (2026-09-25, construct-core 0.18). Was raised when a device that had advertised or used the PQ ratchet saw a bundle stop advertising the unsigned supports_pq_ratchet capability. That capability and its downgrade ledger were deleted the same commit that made the PQ ratchet mandatory wherever post-quantum is built — there is no longer a capability to withdraw, so this prefix is never produced by a current core.—
SENDER_CERTIFICATE_REFUSED / SENDER_DEVICE_MISMATCHA sender certificate failed identity_for_opening (bad signature, key does not derive to the named device, expired past the delivery window), or names another device than the one the message was queued under.Do not open from it. NoTrustedKey is transient: the open is retried once the server key is held.
KEM_IDENTITY_REQUIREDA first flight named no KEM identity key — a core older than the responder's proof of the initiator (Chapter 4 §4.4.4).No session. Upgrade the sender.
KEM_IDENTITY_CHANGEDA first flight named a KEM identity key other than the one pinned for the sending device.No session; a held state is kept. Treat as an impersonation attempt, not a transition.
KEM_IDENTITY_UNAVAILABLEThis device has no hybrid key to derive its KEM identity key from, so it cannot open as initiator.No session. The hybrid key is created with the first bundle upload.
OPEN_SESSION_REFUSEDThe core's answer to SessionBundleFetched, the platform's reply to OpenSession, when the reopen was refused: the message carries the underlying prefix (PQ_REQUIRED: …, a stale or malformed bundle).Nothing — the held session was kept and the open has ended. Retried on a later launch's upgrade sweep.
DECRYPTION_ERROR_UNREADABLEA received DECRYPTION_ERROR could not be opened with our identity key, or is malformed.Nothing — no state is retired and nothing is resent.
decrypt_failedA message could not be read by any held state and carried no handshake header.The core has already produced the DECRYPTION_ERROR (Chapter 5 §5.10) when the message allowed one; the platform only sends it.

A.3 CFE envelope errors — CfeError

Raised when parsing a CFE envelope at the FFI boundary. Defined in construct-core/src/cfe/error.rs. Every entry below is a parse-time or integrity failure and MUST cause the envelope to be rejected before its payload is deserialised.

TagVariantCondition
CFE-TOO-SHORTTooShort { min, got }Buffer is shorter than the 16-byte header.
CFE-INVALID-MAGICInvalidMagicFirst two bytes are not [0x43, 0x46].
CFE-LEGACY-JSONLegacyJsonBuffer begins with { or [ — caller is on a pre-CFE path.
CFE-UNSUP-VERSIONUnsupportedVersion(u8)Version byte is not 0x01.
CFE-UNKNOWN-TYPEUnknownType(u8)msg_type byte does not correspond to any known CfeMessageType variant.
CFE-CRC-MISMATCHChecksumMismatch { stored, computed }CRC-32 over the MessagePack payload bytes does not match the stored value.
CFE-PAYLOAD-TOO-LARGEPayloadTooLarge { max, got }payload_len exceeds the implementation cap (reference: 256 KiB).
CFE-TRUNCATEDTruncatedPayload { expected, got }Buffer ends before payload_len bytes can be read.
CFE-RESERVED-NONZEROInvalidReservedBytesThe three reserved bytes are not [0x00, 0x00, 0x00].
CFE-UNSUP-FLAGSUnsupportedFlags(u8)flags byte is non-zero (no flags defined in v1).
CFE-TYPE-MISMATCHTypeMismatch { expected, got }Decoder was invoked for a specific type but the envelope carries a different one.
CFE-INVALID-FORMATInvalidFormatCatch-all structural error (used by helpers that detect format violations beyond the schema).
CFE-SERIALIZESerializeFailed(String)MessagePack encoder rejected the payload (e.g. unsupported type).
CFE-DESERIALIZEDeserializeFailed(String)MessagePack decoder rejected the payload.
CFE-LEGACY-JSON-PARSELegacyJsonParseFailed(String)A pre-CFE JSON payload could not be parsed even on the migration path.
CFE-B64-DECODEBase64DecodeFailed(String)A base64-encoded field inside a payload failed to decode. Should NOT occur in current binary payloads.
CFE-HEX-DECODEHexDecodeFailed(String)A hex-encoded field failed to decode.
CFE-INVALID-FIELDInvalidField(String)A semantic field-level validation failed inside an otherwise well-formed payload.
CFE-KDF-FAILEDKeyDerivationFailed(String)A KDF step inside a CFE-wrapped operation failed.

CFE-CRC-MISMATCH, CFE-PAYLOAD-TOO-LARGE, and CFE-TRUNCATED are the load-bearing integrity errors: they MUST cause an immediate rejection without any attempt to deserialise the payload.

A.4 WirePayload framing — WirePayloadError

Raised by the WirePayload pack/unpack routines (Chapter 6 §6.2). Defined in construct-core/src/wire_payload.rs (enum WirePayloadError).

TagVariantCondition
WP-INVALID-DHInvalidDhPublicKey(usize)The DH public key field is not exactly 32 bytes (X25519 Montgomery point).
WP-KEM-TOO-LARGEKemTooLarge(usize)KEM ciphertext exceeds u16::MAX bytes (Suite 2 first-message reference value is 1088).
WP-PQ-FIELD-TOO-LARGEPqFieldTooLarge(usize)Suite 4 PQ ratchet EK/CT field exceeds u16::MAX bytes.
WP-PQ-FIELD-TYPEInvalidPqFieldType(u8)Suite 4 PQ section carries a field type other than 0, 1, or 2.
WP-TOO-SHORTTooShort(usize)Buffer is shorter than the 52-byte fixed header, or too short for a length it has already read (a KEM object, the PQ section, pq_key_index's LEB128 bytes).
WP-RETIRED-SUITERetiredSuite(u16)suite_id (after stripping the flag bits) is 0x0003, the epoch-granular PQ ratchet retired in construct-core 0.24.0 (PQR-2, §2.4). Checked before the PQ section is parsed, since that suite's frames have no pq_key_index field to parse.
WP-PQ-INDEX-NO-EPOCHPqKeyIndexWithoutEpoch(u32)pq_key_index is non-zero while pq_message_epoch = 0 — a key index without an epoch to belong to.
WP-PQ-INDEX-NONCANONICALNonCanonicalKeyIndexpq_key_index's LEB128 encoding is not the unique minimal one: more than 5 bytes, a nonzero bit past bit 31, or a final byte that could have been omitted (e.g. 0x80 0x00 for 0). The AD (§5.4) binds the decoded value, not its wire bytes, so a non-minimal encoding would let a relay re-encode the same value undetected.

A WirePayload that fails to parse MUST be dropped without affecting session state. The receiver MUST NOT advance its Double Ratchet on a malformed frame.

Dropped also means finished with: the receiver records the message as processed and acknowledges it, so the server stops redelivering it, and sends nothing back — there is no session state in it a DECRYPTION_ERROR could name. The reference core says so with its own routing verdict, MalformedDropped { message_id } (construct-core 0.28.0), placed before the NotifyError that carries the reason. Until 0.28 the reason was the only answer, which a platform could not tell from "no decision": a suite-3 message from a device on an old build was logged as undecided and left no processed record (2026-10-01). The stream cursor still moved past it, so it was not redelivered.

A.5 Padding errors — PaddingError

Raised by the PKCS#7-style padding helpers (Chapter 5 §5.7). Defined in construct-core/src/traffic_protection/padding.rs:22.

TagVariantCondition
PAD-TOO-LARGEMessageTooLarge(actual, max)Plaintext exceeds MAX_MESSAGE_SIZE (reference: 65 536 bytes).
PAD-INVALIDInvalidPaddingLast byte of the unpadded buffer indicates a length that exceeds the buffer or fails the constant-time unpad check.
PAD-EMPTYEmptyMessageCaller attempted to unpad a zero-byte buffer.

PAD-INVALID MUST be returned in constant time relative to the plaintext length, to avoid leaking padding information via a timing side channel.

A.6 Internal CryptoError

Defined in construct-core/src/error.rs. These variants are not exposed across the FFI surface; they are converted to one of the §A.2 variants at the boundary (From<error::CryptoError> for uniffi_bindings::CryptoError).

VariantApproximate condition
KeyGenerationError(String)RNG failure, dalek keypair generation rejected.
SigningError(String)Ed25519 sign failed (typically wraps ed25519_dalek::SignatureError).
SignatureVerificationError(String)Ed25519 verification failed. Includes the SPK signature check.
KemEncapsulationError(String)ML-KEM Encapsulate failed (ML-KEM-1024 at the handshake, ML-KEM-768 in Suite 4).
KemDecapsulationError(String)ML-KEM Decapsulate failed (malformed ciphertext or wrong key).
AeadEncryptionError(String)ChaCha20-Poly1305 seal failed (typically allocation).
AeadDecryptionError(String)ChaCha20-Poly1305 open failed — tag mismatch, wrong AD, wrong key.
KeyDerivationError(String)HKDF / HMAC-SHA-256 step failed (rare; usually IKM-length validation).
NonceGenerationError(String)RNG returned an error during AEAD nonce sampling.
InvalidInputError(String)Generic input-shape rejection.
SerializationError(String)Internal serialization failure (typically MessagePack).
DeserializationError(String)Internal deserialization failure.
InvalidKeyDataPoint-on-curve or length check failed (mapped to FFI-INVALID-KEY).
InvalidCiphertextStructural ciphertext check failed (mapped to FFI-INVALID-CT).
Other(String)Catch-all.

A.7 Top-level ConstructError

Defined in construct-core/src/utils/error.rs. This is the Result<T> type returned by most public functions in the core; it composes CryptoError via #[from].

VariantApproximate condition
Crypto(CryptoError)Any §A.6 variant, propagated.
StorageError(String)Local persistence (Keychain / SecureStorage / SQLite) failed.
NetworkError(String)Transport-layer call failed before reaching the protocol layer.
SerializationError(String)Top-level serialisation failure (distinct from CryptoError::SerializationError which is per-crypto-step).
ValidationError(String)Domain-level input validation failed (e.g. malformed user id).
SessionError(String)Session state inconsistency (e.g. trying to encrypt before init).
NotFound(String)A keyed lookup failed (user, session, pre-key id).
InvalidInput(String)Caller passed an argument that fails an invariant.
InternalError(String)Should-be-unreachable branch hit; treat as a bug.
NotImplementedA feature stub was called. MUST be surfaced as unimplemented at the FFI surface, not silently swallowed.
Unauthenticated(String)The local state indicates the device is not authenticated (no device_id, no auth token).

A.8 MLS group errors — MlsError

Defined in construct-core/src/group/mls_error.rs. MLS itself is documented at protocol-spec level in a future revision (see Implementation Status §7.1); the errors are listed here so that current implementers know what to expect from the construct-core/src/group/ API surface.

TagVariantCondition
MLS-CRYPTOCryptoError(String)Underlying crypto operation failed inside a group-protocol step.
MLS-EPOCH-MISMATCHEpochMismatchLocal epoch is behind the server; caller MUST FetchCommits and reapply before retrying.
MLS-NOT-MEMBERNotAMemberCaller is not a member of the addressed group.
MLS-SERIALIZESerializationError(String)Group state could not be (de)serialised.
MLS-WELCOMEWelcomeError(String)A Welcome message was invalid, expired, or addressed wrong keys.
MLS-COMMITCommitError(String)A commit could not be applied (stale epoch, invalid signature, etc.).
MLS-CRYPTEncryptionError(String)Application-message encrypt or decrypt failed inside the group.

A.9 VEIL transport errors (informational)

These errors are produced by the construct-veil crate, NOT the protocol core. They are not part of the normative protocol surface and MAY change between releases of construct-veil. They are documented here as a navigation aid for implementers wiring the anti-censorship tier.

A.9.1 Coordinator — CoordinatorError

(construct-veil/src/veil/coordinator.rs)

VariantCondition
Io(std::io::Error)Underlying socket / I/O failure.
Scoring(String)Persistent score store reported an error.
StoppedSession was cancelled by the caller before a backend won.
AllProbesFailedEvery configured backend failed its probe.

A.9.2 Obfuscator — ObfuscatorError

(construct-veil/src/veil/obfuscator.rs)

VariantCondition
IoI/O error during obfuscated handshake or stream.
ConnectionRefusedTCP connect was refused by the relay.
Tls(String)TLS handshake or peer verification failed.
Handshake(String)obfs4 or WebSocket upgrade failed at the application layer.
TimeoutProbe exceeded its time budget.
CancelledProbe was cancelled by the coordinator.
FingerprintBlockedTLS alert 40 / handshake_failure — DPI has classified the method.
WebTunnelDecoyResponseNon-101 on WebSocket upgrade — transparent proxy interception.

A.9.3 Probe failure classification — ProbeFailureReason

(construct-veil/src/veil/fsm/types.rs) — used by the scoring layer to bucket probe outcomes:

FingerprintBlocked · WebTunnelDecoyResponse · TlsCertProblem · ConnectionFailed · Timeout · Unknown.

The sibling enum TransportFailureKind covers steady-state transport degradation (after a probe has already won) and shares the same spelling for the network-observable categories.

A.9.4 Scoring — ScoringError

(construct-veil/src/veil/scoring.rs)

VariantCondition
DbError(String)SQLite persistence error in the per-backend score store.

A.10 gRPC status codes (informational)

Server-to-client errors in the MessagingService / AuthService etc. use standard gRPC status codes. The conventions below are normative for a conforming Konstruct deployment:

gRPC statusNumericKonstruct semantics
OK0Request succeeded.
UNAUTHENTICATED16JWT missing, invalid, or expired. Client MUST clear the in-memory auth state, fall back to RefreshToken, and if that fails treat the device as logged out.
PERMISSION_DENIED7The authenticated identity is not allowed to perform this operation (e.g. wrong device id). Treated equivalently to UNAUTHENTICATED for device-key disposal.
INVALID_ARGUMENT3Request was structurally invalid (e.g. malformed user id). MUST NOT delete local device keys.
NOT_FOUND5Addressed resource (user, pre-key, message) does not exist.
ALREADY_EXISTS6Idempotent create attempted on a resource that already exists (e.g. duplicate registration).
RESOURCE_EXHAUSTED8PoW failed, rate limit hit, or pre-key pool empty. Client MUST back off; SHOULD surface a user-visible cooldown.
FAILED_PRECONDITION9Operation requires earlier state (e.g. encrypt before session init).
ABORTED10Concurrent modification (rare; used for prekey-bundle race resolution).
UNAVAILABLE14Server is starting up, restarting, or routed through a failing relay. Client SHOULD retry with backoff.
INTERNAL13Server-side bug. MUST NOT be auto-retried more than once.
DEADLINE_EXCEEDED4Request took longer than the per-RPC deadline. SHOULD be retried with a fresh deadline.

A.10.1 Auth disposal rules (CRITICAL)

The client MUST distinguish "transport failure" from "server says you are not authenticated":

  • On UNAUTHENTICATED (16) or PERMISSION_DENIED (7) — and only on those codes — the client MUST delete its device keys and trigger re-registration.
  • On any other gRPC error (UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL, etc.) the client MUST NOT touch device keys. These are transient failures.

The reference iOS interceptor implements this distinction in AuthInterceptor.swift; client implementations on other platforms MUST replicate it. Deleting device keys on a transient failure silently logs the user out and forces them through re-registration — this has been a recurring bug in early development; the rule above exists to prevent regressions.

A.11 Diagnostics & logging guidance

When surfacing any error from this registry to the user or to logs:

  1. At the FFI surface (§A.2), the variant tag is the stable contract. Show or log it verbatim. Do not parse the message field for control flow.
  2. Below the FFI surface (§A.6–A.8), the variant names are advisory only. Logs MAY include them for debugging but the application code SHOULD treat the §A.2 variant it eventually receives as the source of truth.
  3. Wire integrity errors (§A.3 CFE-CRC-MISMATCH, §A.4 WP-*) SHOULD be counted as security-relevant log events. A sustained stream of these from a single peer indicates either a misbehaving client or an active attacker injecting bytes.
  4. FFI-SPK-STALE is a user-facing condition, not an internal error. UIs SHOULD render it as "this person hasn't opened Konstruct in N days" rather than as a generic failure dialog.

A.12 Media blobs — MediaError

Raised by seal_media, open_media and the blob-size helpers (Chapter 2 §2.2.3). Defined in construct-core/src/media.rs:53. It does not cross the FFI by name: media_error (construct-core/src/uniffi_bindings.rs:3003) maps each variant to a §A.2 variant, shown in the last column.

TagVariantRaised whenFFI variantClient behaviour
MEDIA-TOO-LARGETooLarge(len)The file is larger than max_plaintext_len() — the 100 000 000-byte blob ceiling less the 36 bytes of nonce, tag and length prefix.EncryptionFailedAsk media_max_plaintext_len() before encoding or uploading, and refuse the attachment up front.
MEDIA-INVALID-KEYInvalidKey(len)The key given to open_media is not MEDIA_KEY_LEN (32) bytes.InvalidKeyDataThe key comes from the message; treat the message's media reference as malformed.
MEDIA-UNAUTHENTICATEDUnauthenticatedThe blob is shorter than nonce plus tag, or opens neither as a v1 blob nor as a pre-0.33 blob under this key.DecryptionFailedDo not render the file. The blob, or the key the message carries, is wrong; retrying with the same pair cannot succeed.
MEDIA-MALFORMED-PADDINGMalformedPaddingThe blob authenticated as v1, but its length prefix does not fit the payload or a padding byte is not zero.InvalidCiphertextDo not render the file. A conforming sealer never writes this.

A legacy blob has no padding, so MEDIA-MALFORMED-PADDING is reachable only for v1.

A.13 Server-signature trust — TrustError

Raised by ServerKeyRing and the delegation functions (Chapter 3 §3.11). Defined in construct-core/src/crypto/server_trust.rs:60. It is not exported through construct_core.udl: admit_server_delegations returns only how many delegations were kept, and the other callers fold each variant into the verdicts of §A.14 and §A.15. A client therefore never matches on it; the table is the reason behind those verdicts.

TagVariantProduced whenFolded into
TRUST-MALFORMEDMalformedDelegation bytes do not decode; or a delegation is issued for a public key that is not hybrid-sized, or with not_before >= not_after.A delegation is not admitted.
TRUST-NO-ROOTNoRootverify_rooted was given an empty root list: this build pins no root (PINNED_ROOTS is empty today).A delegation is not admitted.
TRUST-NOT-ROOTEDNotRootedNo pinned root verifies the delegation's root signature.A delegation is not admitted.
TRUST-UNKNOWN-KEYUnknownKeyNo admitted key has the named key id. Transient: the delegation may not have reached the client yet.NoKey (certificate), Unavailable (tree head)
TRUST-WRONG-PURPOSEWrongPurposeThe key exists but was delegated for another purpose (sender certificate, tree head, sticker manifest).BadSignature / SignatureInvalid
TRUST-OUTSIDE-WINDOWOutsideWindowThe signing time lies outside the delegation's not_before..not_after.BadSignature / SignatureInvalid
TRUST-EXPIREDExpiredThe verifier's clock is past the window's end plus the purpose's grace (Purpose::grace_secs, server-trust/src/lib.rs:99).Expired (certificate), SignatureInvalid (tree head)
TRUST-BAD-SIGNATUREBadSignatureThe hybrid Ed25519 + ML-DSA-65 signature does not verify under the delegated key.BadSignature / SignatureInvalid

An implementation MUST keep UnknownKey distinct from the rest: it is the only variant that says "not yet", and every other one is a refusal.

A.14 Sender-certificate verdict — CertificateVerdict

The answer of certificate_verdict for a SenderCertificate (Chapter 8 §8.3). It is a UDL enum (construct_core.udl, enum CertificateVerdict), defined in construct-core/src/crypto/sealed_sender/mod.rs:421 and derived by SenderCertificate::verdict from the internal SenderRefusal (sealed_sender/mod.rs:279) — the same checks a session opens on, answered as a label.

TagVariantProduced whenClient behaviour
CERT-VOUCHEDVouchedThe signature verifies under an accepted server key and the key derives to the device the certificate names.The certificate may open a session and attest the sender.
CERT-NO-KEYNoKeyNo server key to check with yet (SenderRefusal::NoTrustedKey; TrustError::UnknownKey). Transient.Nothing is dropped. Retry after set_trusted_server_keys / admit_server_delegations have been called.
CERT-BAD-SIGNATUREBadSignatureThe signature fails; the key does not derive to the named device or is not an X25519 key; or a hybrid-signed certificate claims more than one day of lifetime or names a key id of the wrong length.Do not open a session from it and do not attest the sender.
CERT-EXPIREDExpiredSigned, naming its device, and expired longer ago than a message can wait at the relay (MAX_DELIVERY_AGE_SECS, 7 days). Reached only after the signature and device checks pass.Do not open from it. A platform MAY pin the key of an expired-but-signed certificate.

A.15 Key Transparency verdict — KtVerdict

The answer of verify_kt_proofs for one inclusion proof (Chapter 13 §13.3). A UDL enum, defined in construct-core/src/crypto/key_transparency.rs:290 and returned by verify_proof (key_transparency.rs:305). verify_kt_proofs returns one verdict for the X25519 identity key and, when a hybrid identity key was given, a second for it (KtVerdicts { identity, hybrid }).

TagVariantProduced whenClient behaviour
KT-VERIFIEDVerifiedThe leaf is included under the proof's root and the tree head's signature verifies.Record the key as KT-verified.
KT-UNAVAILABLEUnavailableNo key to check the head with yet: no Ed25519 keys handed over, or the head names a key id not admitted.Not evidence of anything.
KT-MALFORMED-PROOFMalformedProofA root or proof hash is not 32 bytes, tree_size is 0, leaf_index >= tree_size, or the head's key id is the wrong length.Record the failure.
KT-INCLUSION-INVALIDInclusionProofInvalidThe recomputed leaf is not under the root; or, with a root pinned, the delegated head names a different tree size or root than the proof.Record the failure.
KT-SIGNATURE-INVALIDSignatureInvalidThe head signature does not verify; or a root is pinned, no delegated head came with the proof and HYBRID_REQUIRED is set.Record the failure.

A failed verdict is recorded on the contact and logged; it does not reject the bundle (Chapter 13 §13.3 step 2).

A.16 History transfer — HistoryError

Why a history transfer between a user's own devices was refused (construct-core/src/history/). The wire name is a UDL [Error] enum (enum HistoryError), defined in construct-core/src/uniffi_bindings.rs:1169 and converted from the core's HistoryFailure (history/mod.rs:48), one case for one case. Each case's message is the specification's name for it (as_str); a client MUST log that name and map the case to text for a person.

TagVariantRaised when
HIST-MALFORMEDMalformedBytes that are not the format, or break a rule of it that has no name of its own.
HIST-TRUNCATEDTruncatedThe stream ended before its End record.
HIST-UNKNOWN-VERSIONUnknownVersionA manifest format_version this reader does not speak.
HIST-USER-MISMATCHUserMismatchA file for another account than this device's.
HIST-RECORD-ORDERRecordOrderA record the phase does not allow, or out of the required order.
HIST-ENVELOPE-MANIFESTEnvelopeManifestMismatchThe manifest names a different snapshot or account than the envelope it arrived in.
HIST-V1-REFUSEDV1RefusedForHistoryA history frame on CTT1 v1, the PIN-HMAC backup handshake.
HIST-IDENTITY-MISMATCHIdentityMismatchKeys or ids in a frame are not the ones the directory holds for that device.
HIST-KEM-KEY-IDKemKeyIdMismatchThe frame was made for a Kyber prekey this device no longer holds.
HIST-QR-PIN-MISMATCHQrPinMismatchThe link QR pinned other keys than the frame carries.
HIST-QR-PIN-ABSENTQrPinAbsentNo QR pin and no bundle-only allowance: nothing authenticates the other device.
HIST-NO-HYBRID-KEYNoHybridKeyThe other device has no hybrid identity key to verify with.
HIST-SIGNATURE-INVALIDSignatureInvalidThe hybrid signature does not verify under the known key.
HIST-CHUNK-OPEN-FAILEDChunkOpenFailedA sealed chunk did not open: wrong key, reordered, spliced or corrupted.
HIST-LOCAL-KEYSLocalKeysUnavailableThis device lacks a key the transfer needs (hybrid identity, Kyber signed prekey).

Every case is a refusal of the transfer at hand; none is a "not yet" like NoKey in §A.14.

A.17 Local store — LocalStoreError

Errors of the core's SQLCipher store that every client keeps. A UDL [Error] enum, defined in construct-core/src/local_store_bindings.rs:12 and converted from store::StoreError.

TagVariantRaised whenClient behaviour
STORE-WRONG-KEYWrongKeyThe key does not open this store.Do not retry with the same key.
STORE-KEY-LENGTHKeyLengthThe store key is not 32 bytes.The store key is 32 random bytes kept by the platform's key store.
STORE-SCHEMA-TOO-NEWSchemaTooNewThe store was written by a newer build than this one.Do not open it with this build.
STORE-CLOSEDClosedThe handle was used after the store was wiped.The handle is dead; do not use it again.
STORE-STORAGEStorage(message)SQLite or file errors, with the detail in the message.Surface as an internal error; do not parse the message.

A.18 Notification peek — PeekVerdict

Not an error: the answer of NotificationPeek::peek for one sealed delivery, decided without acting on it (Chapter 5 §5.8.4). A UDL enum, defined in construct-core/src/orchestration/orchestrator.rs:175.

TagVariantMeaningClient behaviour
PEEK-ALREADY-HANDLEDAlreadyHandledThe app already processed it: its message key is spent in the stored session.Nothing to announce.
PEEK-VISIBLEVisibleA message the app would announce.Show the alert.
PEEK-CONTROLControlA silent control frame, a call signal or a DECRYPTION_ERROR.Nothing to announce.
PEEK-UNREADABLEUnreadableNot read here, for any reason; a first flight or a certificate route is always this.The alert shows.

A.19 References

  • FFI surface: construct-core/src/uniffi_bindings.rs
  • CFE envelope: construct-core/src/cfe/error.rs
  • WirePayload framing: construct-core/src/wire_payload.rs
  • Padding: construct-core/src/traffic_protection/padding.rs
  • Internal crypto: construct-core/src/error.rs
  • Top-level Result: construct-core/src/utils/error.rs
  • MLS: construct-core/src/group/mls_error.rs
  • Media: construct-core/src/media.rs
  • Server-signature trust: construct-core/src/crypto/server_trust.rs
  • Certificate verdict: construct-core/src/crypto/sealed_sender/mod.rs
  • KT verdict: construct-core/src/crypto/key_transparency.rs
  • History transfer: construct-core/src/history/mod.rs, uniffi_bindings.rs
  • Local store: construct-core/src/local_store_bindings.rs
  • Notification peek: construct-core/src/orchestration/orchestrator.rs
  • VEIL coordinator: construct-veil/src/veil/coordinator.rs
  • VEIL obfuscator: construct-veil/src/veil/obfuscator.rs
  • VEIL probe FSM: construct-veil/src/veil/fsm/types.rs
  • VEIL scoring: construct-veil/src/veil/scoring.rs
  • gRPC status codes: https://grpc.io/docs/guides/status-codes/