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
| Surface | Stability |
|---|---|
| §A.2 FFI surface | Stable. Variants MUST NOT be reordered or repurposed. New variants are MINOR additions. |
| §A.3 CFE envelope | Stable. The wire format is fixed; new validation errors are MINOR additions. |
| §A.4 WirePayload | Stable. Header layout is normative. |
| §A.5 Padding | Stable. |
| §A.6–A.8 Internal | Unstable. Variant names and counts MAY change between releases. Catch the FFI surface (§A.2) instead. |
| §A.9 VEIL | Unstable. Tracked in the construct-veil crate. |
| §A.12–A.18 | Added 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 gRPC | Inherited 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.
| Tag | Variant | Raised when | Recovery |
|---|---|---|---|
FFI-INIT | InitializationFailed | The core failed to initialise (e.g. RNG unavailable, keychain locked). | Surface to user; retry after unlock or restart. |
FFI-SESSION-NOT-FOUND | SessionNotFound | An operation referenced a session id that no session exists for in local state. | Trigger a fresh init_session for the peer. |
FFI-SESSION-INIT | SessionInitializationFailed { message } | X3DH / PQXDH handshake construction failed. Wraps a deeper error::CryptoError. | Surface and retry after fetching a fresh prekey bundle. |
FFI-ENCRYPT | EncryptionFailed { message } | RatchetEncrypt failed. Most commonly: session state corruption or AEAD allocation failure. | Surface; do not silently fall back. |
FFI-DECRYPT | DecryptionFailed { 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-KEY | InvalidKeyData | A key field had the wrong length or failed point validation. | Reject the message / bundle. |
FFI-INVALID-CT | InvalidCiphertext | A KEM ciphertext or AEAD frame failed structural validation. | Reject the message. |
FFI-SERIALIZE | SerializationFailed | A MessagePack encode failed for an outbound CFE payload. | Surface as internal error. |
FFI-MSGPACK-DESERIALIZE | MessagePackDeserializationFailed | A MessagePack decode failed on an incoming CFE payload. | Reject the envelope. |
FFI-SPK-STALE | PeerSpkStale { 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 / code | Raised when | Required handling |
|---|---|---|
PQ_REQUIRED | The 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_REFUSED | Removed 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_MISMATCH | A 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_REQUIRED | A 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_CHANGED | A 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_UNAVAILABLE | This 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_REFUSED | The 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_UNREADABLE | A received DECRYPTION_ERROR could not be opened with our identity key, or is malformed. | Nothing — no state is retired and nothing is resent. |
decrypt_failed | A 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.
| Tag | Variant | Condition |
|---|---|---|
CFE-TOO-SHORT | TooShort { min, got } | Buffer is shorter than the 16-byte header. |
CFE-INVALID-MAGIC | InvalidMagic | First two bytes are not [0x43, 0x46]. |
CFE-LEGACY-JSON | LegacyJson | Buffer begins with { or [ — caller is on a pre-CFE path. |
CFE-UNSUP-VERSION | UnsupportedVersion(u8) | Version byte is not 0x01. |
CFE-UNKNOWN-TYPE | UnknownType(u8) | msg_type byte does not correspond to any known CfeMessageType variant. |
CFE-CRC-MISMATCH | ChecksumMismatch { stored, computed } | CRC-32 over the MessagePack payload bytes does not match the stored value. |
CFE-PAYLOAD-TOO-LARGE | PayloadTooLarge { max, got } | payload_len exceeds the implementation cap (reference: 256 KiB). |
CFE-TRUNCATED | TruncatedPayload { expected, got } | Buffer ends before payload_len bytes can be read. |
CFE-RESERVED-NONZERO | InvalidReservedBytes | The three reserved bytes are not [0x00, 0x00, 0x00]. |
CFE-UNSUP-FLAGS | UnsupportedFlags(u8) | flags byte is non-zero (no flags defined in v1). |
CFE-TYPE-MISMATCH | TypeMismatch { expected, got } | Decoder was invoked for a specific type but the envelope carries a different one. |
CFE-INVALID-FORMAT | InvalidFormat | Catch-all structural error (used by helpers that detect format violations beyond the schema). |
CFE-SERIALIZE | SerializeFailed(String) | MessagePack encoder rejected the payload (e.g. unsupported type). |
CFE-DESERIALIZE | DeserializeFailed(String) | MessagePack decoder rejected the payload. |
CFE-LEGACY-JSON-PARSE | LegacyJsonParseFailed(String) | A pre-CFE JSON payload could not be parsed even on the migration path. |
CFE-B64-DECODE | Base64DecodeFailed(String) | A base64-encoded field inside a payload failed to decode. Should NOT occur in current binary payloads. |
CFE-HEX-DECODE | HexDecodeFailed(String) | A hex-encoded field failed to decode. |
CFE-INVALID-FIELD | InvalidField(String) | A semantic field-level validation failed inside an otherwise well-formed payload. |
CFE-KDF-FAILED | KeyDerivationFailed(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).
| Tag | Variant | Condition |
|---|---|---|
WP-INVALID-DH | InvalidDhPublicKey(usize) | The DH public key field is not exactly 32 bytes (X25519 Montgomery point). |
WP-KEM-TOO-LARGE | KemTooLarge(usize) | KEM ciphertext exceeds u16::MAX bytes (Suite 2 first-message reference value is 1088). |
WP-PQ-FIELD-TOO-LARGE | PqFieldTooLarge(usize) | Suite 4 PQ ratchet EK/CT field exceeds u16::MAX bytes. |
WP-PQ-FIELD-TYPE | InvalidPqFieldType(u8) | Suite 4 PQ section carries a field type other than 0, 1, or 2. |
WP-TOO-SHORT | TooShort(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-SUITE | RetiredSuite(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-EPOCH | PqKeyIndexWithoutEpoch(u32) | pq_key_index is non-zero while pq_message_epoch = 0 — a key index without an epoch to belong to. |
WP-PQ-INDEX-NONCANONICAL | NonCanonicalKeyIndex | pq_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.
| Tag | Variant | Condition |
|---|---|---|
PAD-TOO-LARGE | MessageTooLarge(actual, max) | Plaintext exceeds MAX_MESSAGE_SIZE (reference: 65 536 bytes). |
PAD-INVALID | InvalidPadding | Last byte of the unpadded buffer indicates a length that exceeds the buffer or fails the constant-time unpad check. |
PAD-EMPTY | EmptyMessage | Caller 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).
| Variant | Approximate 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. |
InvalidKeyData | Point-on-curve or length check failed (mapped to FFI-INVALID-KEY). |
InvalidCiphertext | Structural 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].
| Variant | Approximate 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. |
NotImplemented | A 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.
| Tag | Variant | Condition |
|---|---|---|
MLS-CRYPTO | CryptoError(String) | Underlying crypto operation failed inside a group-protocol step. |
MLS-EPOCH-MISMATCH | EpochMismatch | Local epoch is behind the server; caller MUST FetchCommits and reapply before retrying. |
MLS-NOT-MEMBER | NotAMember | Caller is not a member of the addressed group. |
MLS-SERIALIZE | SerializationError(String) | Group state could not be (de)serialised. |
MLS-WELCOME | WelcomeError(String) | A Welcome message was invalid, expired, or addressed wrong keys. |
MLS-COMMIT | CommitError(String) | A commit could not be applied (stale epoch, invalid signature, etc.). |
MLS-CRYPT | EncryptionError(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)
| Variant | Condition |
|---|---|
Io(std::io::Error) | Underlying socket / I/O failure. |
Scoring(String) | Persistent score store reported an error. |
Stopped | Session was cancelled by the caller before a backend won. |
AllProbesFailed | Every configured backend failed its probe. |
A.9.2 Obfuscator — ObfuscatorError
(construct-veil/src/veil/obfuscator.rs)
| Variant | Condition |
|---|---|
Io | I/O error during obfuscated handshake or stream. |
ConnectionRefused | TCP 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. |
Timeout | Probe exceeded its time budget. |
Cancelled | Probe was cancelled by the coordinator. |
FingerprintBlocked | TLS alert 40 / handshake_failure — DPI has classified the method. |
WebTunnelDecoyResponse | Non-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)
| Variant | Condition |
|---|---|
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 status | Numeric | Konstruct semantics |
|---|---|---|
OK | 0 | Request succeeded. |
UNAUTHENTICATED | 16 | JWT 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_DENIED | 7 | The authenticated identity is not allowed to perform this operation (e.g. wrong device id). Treated equivalently to UNAUTHENTICATED for device-key disposal. |
INVALID_ARGUMENT | 3 | Request was structurally invalid (e.g. malformed user id). MUST NOT delete local device keys. |
NOT_FOUND | 5 | Addressed resource (user, pre-key, message) does not exist. |
ALREADY_EXISTS | 6 | Idempotent create attempted on a resource that already exists (e.g. duplicate registration). |
RESOURCE_EXHAUSTED | 8 | PoW failed, rate limit hit, or pre-key pool empty. Client MUST back off; SHOULD surface a user-visible cooldown. |
FAILED_PRECONDITION | 9 | Operation requires earlier state (e.g. encrypt before session init). |
ABORTED | 10 | Concurrent modification (rare; used for prekey-bundle race resolution). |
UNAVAILABLE | 14 | Server is starting up, restarting, or routed through a failing relay. Client SHOULD retry with backoff. |
INTERNAL | 13 | Server-side bug. MUST NOT be auto-retried more than once. |
DEADLINE_EXCEEDED | 4 | Request 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) orPERMISSION_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:
- At the FFI surface (§A.2), the variant tag is the stable
contract. Show or log it verbatim. Do not parse the
messagefield for control flow. - 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.
- Wire integrity errors (§A.3
CFE-CRC-MISMATCH, §A.4WP-*) 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. FFI-SPK-STALEis 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.
| Tag | Variant | Raised when | FFI variant | Client behaviour |
|---|---|---|---|---|
MEDIA-TOO-LARGE | TooLarge(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. | EncryptionFailed | Ask media_max_plaintext_len() before encoding or uploading, and refuse the attachment up front. |
MEDIA-INVALID-KEY | InvalidKey(len) | The key given to open_media is not MEDIA_KEY_LEN (32) bytes. | InvalidKeyData | The key comes from the message; treat the message's media reference as malformed. |
MEDIA-UNAUTHENTICATED | Unauthenticated | The blob is shorter than nonce plus tag, or opens neither as a v1 blob nor as a pre-0.33 blob under this key. | DecryptionFailed | Do not render the file. The blob, or the key the message carries, is wrong; retrying with the same pair cannot succeed. |
MEDIA-MALFORMED-PADDING | MalformedPadding | The blob authenticated as v1, but its length prefix does not fit the payload or a padding byte is not zero. | InvalidCiphertext | Do 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.
| Tag | Variant | Produced when | Folded into |
|---|---|---|---|
TRUST-MALFORMED | Malformed | Delegation 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-ROOT | NoRoot | verify_rooted was given an empty root list: this build pins no root (PINNED_ROOTS is empty today). | A delegation is not admitted. |
TRUST-NOT-ROOTED | NotRooted | No pinned root verifies the delegation's root signature. | A delegation is not admitted. |
TRUST-UNKNOWN-KEY | UnknownKey | No admitted key has the named key id. Transient: the delegation may not have reached the client yet. | NoKey (certificate), Unavailable (tree head) |
TRUST-WRONG-PURPOSE | WrongPurpose | The key exists but was delegated for another purpose (sender certificate, tree head, sticker manifest). | BadSignature / SignatureInvalid |
TRUST-OUTSIDE-WINDOW | OutsideWindow | The signing time lies outside the delegation's not_before..not_after. | BadSignature / SignatureInvalid |
TRUST-EXPIRED | Expired | The 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-SIGNATURE | BadSignature | The 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.
| Tag | Variant | Produced when | Client behaviour |
|---|---|---|---|
CERT-VOUCHED | Vouched | The 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-KEY | NoKey | No 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-SIGNATURE | BadSignature | The 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-EXPIRED | Expired | Signed, 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 }).
| Tag | Variant | Produced when | Client behaviour |
|---|---|---|---|
KT-VERIFIED | Verified | The leaf is included under the proof's root and the tree head's signature verifies. | Record the key as KT-verified. |
KT-UNAVAILABLE | Unavailable | No 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-PROOF | MalformedProof | A 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-INVALID | InclusionProofInvalid | The 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-INVALID | SignatureInvalid | The 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.
| Tag | Variant | Raised when |
|---|---|---|
HIST-MALFORMED | Malformed | Bytes that are not the format, or break a rule of it that has no name of its own. |
HIST-TRUNCATED | Truncated | The stream ended before its End record. |
HIST-UNKNOWN-VERSION | UnknownVersion | A manifest format_version this reader does not speak. |
HIST-USER-MISMATCH | UserMismatch | A file for another account than this device's. |
HIST-RECORD-ORDER | RecordOrder | A record the phase does not allow, or out of the required order. |
HIST-ENVELOPE-MANIFEST | EnvelopeManifestMismatch | The manifest names a different snapshot or account than the envelope it arrived in. |
HIST-V1-REFUSED | V1RefusedForHistory | A history frame on CTT1 v1, the PIN-HMAC backup handshake. |
HIST-IDENTITY-MISMATCH | IdentityMismatch | Keys or ids in a frame are not the ones the directory holds for that device. |
HIST-KEM-KEY-ID | KemKeyIdMismatch | The frame was made for a Kyber prekey this device no longer holds. |
HIST-QR-PIN-MISMATCH | QrPinMismatch | The link QR pinned other keys than the frame carries. |
HIST-QR-PIN-ABSENT | QrPinAbsent | No QR pin and no bundle-only allowance: nothing authenticates the other device. |
HIST-NO-HYBRID-KEY | NoHybridKey | The other device has no hybrid identity key to verify with. |
HIST-SIGNATURE-INVALID | SignatureInvalid | The hybrid signature does not verify under the known key. |
HIST-CHUNK-OPEN-FAILED | ChunkOpenFailed | A sealed chunk did not open: wrong key, reordered, spliced or corrupted. |
HIST-LOCAL-KEYS | LocalKeysUnavailable | This 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.
| Tag | Variant | Raised when | Client behaviour |
|---|---|---|---|
STORE-WRONG-KEY | WrongKey | The key does not open this store. | Do not retry with the same key. |
STORE-KEY-LENGTH | KeyLength | The store key is not 32 bytes. | The store key is 32 random bytes kept by the platform's key store. |
STORE-SCHEMA-TOO-NEW | SchemaTooNew | The store was written by a newer build than this one. | Do not open it with this build. |
STORE-CLOSED | Closed | The handle was used after the store was wiped. | The handle is dead; do not use it again. |
STORE-STORAGE | Storage(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.
| Tag | Variant | Meaning | Client behaviour |
|---|---|---|---|
PEEK-ALREADY-HANDLED | AlreadyHandled | The app already processed it: its message key is spent in the stored session. | Nothing to announce. |
PEEK-VISIBLE | Visible | A message the app would announce. | Show the alert. |
PEEK-CONTROL | Control | A silent control frame, a call signal or a DECRYPTION_ERROR. | Nothing to announce. |
PEEK-UNREADABLE | Unreadable | Not 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/