FIDO2 Error Handling - FeitianTech/postquantum-webauthn-platform GitHub Wiki
- Introduction
- CTAP2 Error Classification
- CtapError Class Implementation
- ClientError Class and Error Mapping
- Transport Layer Error Handling
- Command Execution Error Propagation
- Common Error Scenarios
- Testing Error Conditions
- Best Practices for Error Handling
- Logging and Debugging
The CTAP2 (Client to Authenticator Protocol) implementation in this WebAuthn platform provides a comprehensive error handling system that propagates errors from the authenticator through multiple layers of abstraction, ultimately reaching the application layer. The system is designed to handle various types of failures gracefully, from low-level transport errors to high-level application logic errors.
The error handling architecture follows a layered approach where each layer can transform, log, or propagate errors according to its role in the system. This ensures that applications receive meaningful error information while maintaining the flexibility to handle different types of failures appropriately.
The CTAP2 protocol defines a comprehensive set of status codes that categorize different types of errors that can occur during authenticator operations. These errors are systematically organized into logical groups for easier handling and interpretation.
The CtapError.ERR enumeration defines the standard CTAP2 error codes:
| Error Code | Constant | Description |
|---|---|---|
| 0x00 | SUCCESS | Operation successful |
| 0x01 | INVALID_COMMAND | Command not supported |
| 0x02 | INVALID_PARAMETER | Parameter invalid |
| 0x03 | INVALID_LENGTH | Data length error |
| 0x04 | INVALID_SEQ | Invalid sequence number |
| 0x05 | TIMEOUT | Operation timed out |
| 0x06 | CHANNEL_BUSY | Channel temporarily busy |
| 0x0A | LOCK_REQUIRED | Device locked |
| 0x0B | INVALID_CHANNEL | Invalid channel ID |
| 0x11 | CBOR_UNEXPECTED_TYPE | CBOR type mismatch |
| 0x12 | INVALID_CBOR | Malformed CBOR data |
| 0x14 | MISSING_PARAMETER | Required parameter missing |
| 0x15 | LIMIT_EXCEEDED | Resource limit exceeded |
| 0x17 | FP_DATABASE_FULL | Fingerprint database full |
| 0x18 | LARGE_BLOB_STORAGE_FULL | Large blob storage full |
| 0x19 | CREDENTIAL_EXCLUDED | Credential excluded |
| 0x21 | PROCESSING | Processing in progress |
| 0x22 | INVALID_CREDENTIAL | Credential invalid |
| 0x23 | USER_ACTION_PENDING | User action required |
| 0x24 | OPERATION_PENDING | Operation pending |
| 0x25 | NO_OPERATIONS | No operations pending |
| 0x26 | UNSUPPORTED_ALGORITHM | Algorithm not supported |
| 0x27 | OPERATION_DENIED | Operation denied |
| 0x28 | KEY_STORE_FULL | Key storage full |
| 0x2B | UNSUPPORTED_OPTION | Option not supported |
| 0x2C | INVALID_OPTION | Invalid option |
| 0x2D | KEEPALIVE_CANCEL | Keep-alive canceled |
| 0x2E | NO_CREDENTIALS | No credentials available |
| 0x2F | USER_ACTION_TIMEOUT | User action timeout |
| 0x30 | NOT_ALLOWED | Operation not allowed |
| 0x31-0x35 | PIN_* | PIN-related errors |
| 0x36 | PUAT_REQUIRED | Platform U2F token required |
| 0x37 | PIN_POLICY_VIOLATION | PIN policy violation |
| 0x38 | PIN_TOKEN_EXPIRED | PIN token expired |
| 0x39 | REQUEST_TOO_LARGE | Request exceeds size limits |
| 0x3A | ACTION_TIMEOUT | Action timeout |
| 0x3B | UP_REQUIRED | User presence required |
| 0x3C | UV_BLOCKED | User verification blocked |
| 0x3D | INTEGRITY_FAILURE | Data integrity check failed |
| 0x3E | INVALID_SUBCOMMAND | Invalid subcommand |
| 0x3F | UV_INVALID | User verification invalid |
| 0x40 | UNAUTHORIZED_PERMISSION | Unauthorized permission |
The protocol also reserves ranges for extension and vendor-specific error codes:
- Extension Range: 0xE0-0xEF
- Vendor Range: 0xF0-0xFF
- Other: 0x7F (generic error)
Section sources
- fido2/ctap.py
The CtapError class serves as the primary exception type for CTAP2 protocol errors. It provides a structured way to handle and represent authenticator-reported errors throughout the system.
classDiagram
class CtapError {
+ERR code
+__init__(code : int)
+__str__() str
}
class ERR {
<<enumeration>>
+SUCCESS : 0x00
+INVALID_COMMAND : 0x01
+INVALID_PARAMETER : 0x02
+TIMEOUT : 0x05
+CHANNEL_BUSY : 0x06
+PIN_INVALID : 0x31
+USER_ACTION_TIMEOUT : 0x2F
+OTHER : 0x7F
+__str__() str
}
class UNKNOWN_ERR {
+value : int
+name : "UNKNOWN_ERR"
+__repr__() str
+__str__() str
}
CtapError --> ERR : "uses"
CtapError --> UNKNOWN_ERR : "fallback for unknown codes"
Diagram sources
- fido2/ctap.py
The constructor implements robust error code validation:
-
Known Error Codes: Attempts to convert the integer code to a predefined
CtapError.ERRenumeration value -
Unknown Error Codes: Falls back to
CtapError.UNKNOWN_ERRfor unrecognized codes -
String Representation: Provides human-readable error descriptions through the
__str__method
The UNKNOWN_ERR class provides graceful handling for unrecognized error codes:
- Fallback Mechanism: Ensures all error codes are represented
- Consistent Interface: Maintains the same interface as known error codes
- Debugging Support: Preserves the original numeric value for debugging
Section sources
- fido2/ctap.py
The ClientError class provides a higher-level abstraction for application-facing errors, transforming CTAP2 errors into more meaningful categories for client applications.
The ClientError.ERR enumeration defines five primary error categories:
| Error Code | Category | Description |
|---|---|---|
| OTHER_ERROR | Generic | Unspecified errors |
| BAD_REQUEST | Client-side | Invalid requests or parameters |
| CONFIGURATION_UNSUPPORTED | Configuration | Unsupported features or options |
| DEVICE_INELIGIBLE | Device | Device-specific issues |
| TIMEOUT | Timing | Timeout-related failures |
The _ctap2client_err function implements sophisticated error mapping logic:
flowchart TD
A[CTAP2 Error] --> B{Error Classification}
B --> |CREDENTIAL_EXCLUDED,<br/>NO_CREDENTIALS| C[DEVICE_INELIGIBLE]
B --> |KEEPALIVE_CANCEL,<br/>ACTION_TIMEOUT,<br/>USER_ACTION_TIMEOUT| D[TIMEOUT]
B --> |UNSUPPORTED_ALGORITHM,<br/>UNSUPPORTED_OPTION,<br/>KEY_STORE_FULL| E[CONFIGURATION_UNSUPPORTED]
B --> |INVALID_COMMAND,<br/>PIN_* errors,<br/>REQUEST_TOO_LARGE| F[BAD_REQUEST]
B --> |Other| G[OTHER_ERROR]
C --> H[ClientError Instance]
D --> H
E --> H
F --> H
G --> H
Diagram sources
- fido2/client/init.py
- CREDENTIAL_EXCLUDED: Credential is excluded from operations
- NO_CREDENTIALS: No suitable credentials available
-
Result: Transformed to
CLIENTERROR.ERR.DEVICE_INELIGIBLE
- KEEPALIVE_CANCEL: User canceled operation via keep-alive
- ACTION_TIMEOUT: Action timed out
- USER_ACTION_TIMEOUT: User action timeout
-
Result: Transformed to
CLIENTERROR.ERR.TIMEOUT
- UNSUPPORTED_ALGORITHM: Algorithm not supported by device
- UNSUPPORTED_OPTION: Feature not supported
- KEY_STORE_FULL: Storage capacity exceeded
-
Result: Transformed to
CLIENTERROR.ERR.CONFIGURATION_UNSUPPORTED
- INVALID_COMMAND: Invalid command sent to device
- PIN_ errors*: Various PIN-related failures
- REQUEST_TOO_LARGE: Request exceeds size limits
-
Result: Transformed to
CLIENTERROR.ERR.BAD_REQUEST
Section sources
- fido2/client/init.py
The HID transport layer implements robust error handling mechanisms to deal with communication failures, connection issues, and protocol violations.
The ConnectionFailure exception handles transport-level failures:
- Nonce Mismatch: Invalid initialization response
- Channel Errors: Wrong channel ID in responses
- Sequence Violations: Incorrect packet sequencing
- Protocol Violations: Invalid packet formats
The transport layer implements automatic retry logic for CHANNEL_BUSY errors:
sequenceDiagram
participant App as Application
participant Transport as HID Transport
participant Device as Authenticator
App->>Transport : send_cbor(command)
Transport->>Device : send packet
Device-->>Transport : CHANNEL_BUSY
Transport->>Transport : wait(0.1s)
Transport->>Device : retry packet
Device-->>Transport : success/error
Transport-->>App : result/error
Diagram sources
- fido2/hid/init.py
The transport layer handles KEEPALIVE messages for long-running operations:
- Status Extraction: Parses keep-alive status from response packets
- Callback Invocation: Calls application-provided callbacks
- Duplicate Filtering: Prevents redundant callback invocations
- Error Handling: Validates keep-alive status codes
Section sources
- fido2/hid/init.py
The send_cbor method demonstrates comprehensive error propagation through the CTAP2 command execution pipeline.
sequenceDiagram
participant App as Application
participant CTAP2 as Ctap2 Class
participant Device as CtapDevice
participant Transport as HID Transport
App->>CTAP2 : send_cbor(cmd, data)
CTAP2->>CTAP2 : validate_message_size()
CTAP2->>Device : call(CTAPHID.CBOR, request)
Device->>Transport : write_packet(request)
Transport->>Transport : send_request()
Transport-->>Device : response_packet
Device->>Device : parse_response()
alt Status == SUCCESS
Device-->>CTAP2 : response_data
CTAP2->>CTAP2 : decode_cbor(response_data)
CTAP2-->>App : decoded_result
else Status != SUCCESS
Device-->>CTAP2 : error_status
CTAP2->>CTAP2 : raise CtapError(error_status)
CTAP2-->>App : CtapError
end
Diagram sources
- fido2/ctap2/base.py
The send_cbor method validates message size before transmission:
if len(request) > self._max_msg_size:
raise CtapError(CtapError.ERR.REQUEST_TOO_LARGE)After receiving the response, the system checks the status byte:
status = response[0]
if status != 0x00:
raise CtapError(status)The system performs strict CBOR validation when strict_cbor is enabled:
if self._strict_cbor:
expected = cbor.encode(decoded)
if expected != enc:
raise ValueError("Non-canonical CBOR from Authenticator")Different CTAP2 commands implement specialized error handling:
-
Credential Exclusion:
CtapError.ERR.CREDENTIAL_EXCLUDED -
Algorithm Unsupported:
CtapError.ERR.UNSUPPORTED_ALGORITHM -
Storage Full:
CtapError.ERR.KEY_STORE_FULL
-
No Credentials:
CtapError.ERR.NO_CREDENTIALS -
User Presence Required:
CtapError.ERR.UP_REQUIRED
Section sources
- fido2/ctap2/base.py
Timeout errors are among the most common failure modes in CTAP2 operations. They can occur at multiple levels:
-
Symptoms:
CtapError.ERR.UP_REQUIREDorCtapError.ERR.USER_ACTION_TIMEOUT - Causes: User didn't provide required biometric or touch input
- Resolution: Implement retry logic with user guidance
-
Symptoms:
CtapError.ERR.PROCESSINGtimeouts - Causes: Complex cryptographic operations exceeding timeout limits
- Resolution: Increase timeout values or simplify operations
PIN-related errors require careful handling for security and usability:
-
PIN_INVALID:
CtapError.ERR.PIN_INVALID -
PIN_BLOCKED:
CtapError.ERR.PIN_BLOCKED -
PIN_NOT_SET:
CtapError.ERR.PIN_NOT_SET
-
PIN_POLICY_VIOLATION:
CtapError.ERR.PIN_POLICY_VIOLATION -
PIN_TOKEN_EXPIRED:
CtapError.ERR.PIN_TOKEN_EXPIRED
Transport-level errors can occur due to hardware or communication issues:
- Device Disconnected: Lost USB/HID connection
- Packet Corruption: Malformed packets during transmission
- Channel Conflicts: Multiple applications competing for device access
-
Request Too Large:
CtapError.ERR.REQUEST_TOO_LARGE - Response Buffer Overflow: Insufficient buffer space for responses
Section sources
- fido2/ctap.py
The test suite provides comprehensive coverage of error handling scenarios through mocked authenticator responses.
Test cases simulate various error conditions by configuring mock device responses:
# Simulate timeout error
device.call.return_value = b"\x05" # TIMEOUT error code
# Simulate PIN invalid error
device.call.return_value = b"\x31" # PIN_INVALID error code
# Simulate credential excluded error
device.call.return_value = b"\x19" # CREDENTIAL_EXCLUDED error codeThe TestCtap2 class demonstrates error propagation testing:
def test_send_cbor_error(self):
ctap = self.mock_ctap()
ctap.device.call.return_value = b"\x05" # TIMEOUT error
with self.assertRaises(CtapError) as cm:
ctap.send_cbor(2, b"foobar")
self.assertEqual(cm.exception.code, CtapError.ERR.TIMEOUT)The transport layer includes specific tests for keep-alive error handling:
def test_invalid_keepalive_status(self):
# Test malformed keep-alive status
recv = struct.pack(">IB", self._channel_id,
TYPE_INIT | CTAPHID.KEEPALIVE)
recv += b"\xFF" # Invalid status code
with self.assertRaises(ConnectionFailure):
self._process_keepalive(recv)Section sources
- tests/test_ctap2.py
- tests/test_hid.py
Implement fallback mechanisms for non-critical operations:
try:
result = authenticator.make_credential(client_data, rp, user)
except ClientError as e:
if e.code == ClientError.ERR.TIMEOUT:
# Fallback to simpler authentication
result = fallback_authentication()
elif e.code == ClientError.ERR.CONFIGURATION_UNSUPPORTED:
# Disable unsupported features
disable_unsupported_features()
else:
raise # Re-raise unexpected errorsConvert technical error codes to user-understandable messages:
def get_user_friendly_error(client_error):
error_map = {
ClientError.ERR.TIMEOUT: "Operation timed out. Please try again.",
ClientError.ERR.DEVICE_INELIGIBLE: "No suitable credentials found.",
ClientError.ERR.BAD_REQUEST: "Invalid request parameters.",
ClientError.ERR.CONFIGURATION_UNSUPPORTED: "Feature not supported by your device."
}
return error_map.get(client_error.code, "An unexpected error occurred.")Implement intelligent retry logic for transient errors:
def execute_with_retry(operation, max_retries=3, backoff_factor=1.5):
for attempt in range(max_retries):
try:
return operation()
except CtapError as e:
if e.code == CtapError.ERR.CHANNEL_BUSY and attempt < max_retries - 1:
time.sleep(backoff_factor ** attempt)
continue
raiseFor recoverable errors, implement state reset mechanisms:
def handle_recovery_error(error):
if isinstance(error, CtapError) and error.code in [
CtapError.ERR.CHANNEL_BUSY,
CtapError.ERR.INVALID_CHANNEL
]:
# Reset device connection
authenticator.reset_connection()
return True
return FalseHandle partial failures gracefully:
def process_multiple_credentials(operations):
successes = []
failures = []
for op in operations:
try:
result = op.execute()
successes.append(result)
except CtapError as e:
if e.code == CtapError.ERR.NO_CREDENTIALS:
# Continue with next credential
continue
failures.append((op, e))
return successes, failuresAvoid exposing sensitive information in error messages:
def secure_error_handling(operation):
try:
return operation()
except CtapError as e:
# Log technical details privately
logger.debug(f"CTAP2 error: {e}")
# Return generic error to user
raise ClientError(ERR.OTHER_ERROR, "Authentication failed")Implement rate limiting to prevent abuse:
import time
from collections import defaultdict
class ErrorRateLimiter:
def __init__(self, max_errors=5, window_seconds=60):
self.max_errors = max_errors
self.window_seconds = window_seconds
self.error_counts = defaultdict(list)
def record_error(self, client_id):
now = time.time()
self.error_counts[client_id].append(now)
# Remove old errors outside time window
cutoff = now - self.window_seconds
self.error_counts[client_id] = [
timestamp for timestamp in self.error_counts[client_id]
if timestamp > cutoff
]
def is_allowed(self, client_id):
return len(self.error_counts[client_id]) < self.max_errorsThe system implements structured logging for comprehensive error tracking:
The HID transport layer logs all packet traffic for debugging:
logger.log(LOG_LEVEL_TRAFFIC, "SEND: %s", packet.hex())
logger.log(LOG_LEVEL_TRAFFIC, "RECV: %s", recv.hex())Capture contextual information with errors:
try:
result = authenticator.make_credential(client_data, rp, user)
except CtapError as e:
logger.error(f"Make credential failed for user {user['id']}: {e}")
logger.debug(f"RP: {rp}, Client data hash: {client_data.hash.hex()}")
raiseLog device capabilities for troubleshooting:
def log_device_capabilities(authenticator):
info = authenticator.get_info()
logger.info(f"Device capabilities: {info.options}")
logger.info(f"Supported algorithms: {[a['alg'] for a in info.algorithms]}")
logger.info(f"Max message size: {info.max_msg_size}")Track individual transactions for debugging:
class TransactionTracer:
def __init__(self):
self.transactions = []
def start_transaction(self, operation, params):
transaction = {
'operation': operation,
'params': params,
'start_time': time.time(),
'steps': []
}
self.current_transaction = transaction
return transaction
def add_step(self, step_name, result):
self.current_transaction['steps'].append({
'step': step_name,
'result': result,
'timestamp': time.time()
})
def complete_transaction(self, success, error=None):
self.current_transaction['duration'] = time.time() - self.current_transaction['start_time']
self.current_transaction['success'] = success
self.current_transaction['error'] = error
if not success:
logger.error(f"Transaction failed: {self.current_transaction}")
else:
logger.debug(f"Transaction completed: {self.current_transaction}")Implement diagnostic tools for error analysis:
class ErrorHandlerAnalyzer:
def __init__(self):
self.error_stats = defaultdict(lambda: {'count': 0, 'last_seen': None})
def record_error(self, error_code, context=None):
self.error_stats[error_code]['count'] += 1
self.error_stats[error_code]['last_seen'] = time.time()
self.error_stats[error_code]['context'] = context
def get_error_summary(self):
return {
code: {
'count': stats['count'],
'rate': stats['count'] / (time.time() - stats['last_seen'])
if stats['last_seen'] else 0
}
for code, stats in self.error_stats.items()
}Monitor system health through error patterns:
class HealthMonitor:
def __init__(self, alert_threshold=10):
self.alert_threshold = alert_threshold
self.error_counts = defaultdict(int)
def check_health(self):
critical_errors = [
code for code, count in self.error_counts.items()
if count > self.alert_threshold
]
if critical_errors:
return {
'status': 'critical',
'errors': critical_errors,
'message': f"High error rate detected: {critical_errors}"
}
return {'status': 'healthy'}Section sources
- fido2/hid/init.py
- fido2/client/init.py