Python library for communicating with Yubico YubiKey hardware authentication tokens
—
HMAC-SHA1 and Yubico challenge-response authentication for secure authentication workflows, supporting both variable and fixed-length responses with configurable user interaction requirements.
Perform challenge-response operations using pre-configured YubiKey slots.
# Base YubiKey class method
def challenge(challenge, mode='HMAC', slot=1, variable=True, may_block=True):
"""
Issue challenge to YubiKey and return response (base class method).
Requires YubiKey 2.2+ with challenge-response configuration in specified slot.
Parameters:
- challenge (bytes): Challenge data to send to YubiKey
- HMAC mode: Up to 64 bytes
- OTP mode: Exactly 6 bytes
- mode (str): Challenge mode
- 'HMAC': HMAC-SHA1 challenge-response
- 'OTP': Yubico challenge-response
- slot (int): Configuration slot number (1 or 2)
- variable (bool): Variable length response (HMAC mode only)
- True: Response length matches challenge length
- False: Fixed 20-byte response
- may_block (bool): Allow operations requiring user interaction
- True: May wait for button press if configured
- False: Fail immediately if button press required
Returns:
bytes: Response data
- HMAC mode: 20 bytes (fixed) or variable length
- OTP mode: 16 bytes
Raises:
YubiKeyVersionError: Device doesn't support challenge-response
YubiKeyTimeout: Operation timed out waiting for user interaction
InputError: Challenge data is invalid for specified mode
YubiKeyError: Challenge-response operation failed
"""
# USB HID implementation method (same signature)
def challenge_response(challenge, mode='HMAC', slot=1, variable=True, may_block=True):
"""
Issue challenge to YubiKey and return response (USB HID implementation).
Same functionality as challenge() method but available on USB HID implementations.
Both methods can be used interchangeably.
Parameters: Same as challenge() method
Returns: Same as challenge() method
Raises: Same as challenge() method
"""HMAC-SHA1 provides cryptographically strong challenge-response authentication.
Usage example:
import yubico
import hashlib
import hmac
import os
# Connect to YubiKey
yk = yubico.find_yubikey()
# Generate random challenge
challenge = os.urandom(32)
try:
# Perform HMAC challenge-response (works with both challenge() and challenge_response())
response = yk.challenge(
challenge=challenge,
mode='HMAC',
slot=1,
variable=True
)
print(f"Challenge: {challenge.hex()}")
print(f"Response: {response.hex()}")
print(f"Response length: {len(response)} bytes")
except yubico.yubikey_base.YubiKeyVersionError:
print("YubiKey doesn't support challenge-response")
except yubico.yubico_exception.InputError as e:
print(f"Invalid challenge data: {e.reason}")Yubico challenge-response uses a proprietary algorithm for compatibility with legacy systems.
Usage example:
import yubico
yk = yubico.find_yubikey()
# Yubico challenge-response requires exactly 6 bytes
challenge = b"123456"
try:
response = yk.challenge(
challenge=challenge,
mode='OTP',
slot=2,
may_block=True
)
print(f"Challenge: {challenge.hex()}")
print(f"Response: {response.hex()}")
print(f"Response length: {len(response)} bytes") # Always 16 bytes
except yubico.yubikey_base.YubiKeyVersionError:
print("YubiKey doesn't support Yubico challenge-response")
except yubico.yubico_exception.InputError:
print("Challenge must be exactly 6 bytes for OTP mode")HMAC mode supports both variable and fixed-length responses.
import yubico
yk = yubico.find_yubikey()
challenge = b"test challenge data"
# Variable length response (matches challenge length)
var_response = yk.challenge_response(
challenge=challenge,
mode='HMAC',
slot=1,
variable=True
)
# Fixed length response (always 20 bytes)
fixed_response = yk.challenge_response(
challenge=challenge,
mode='HMAC',
slot=1,
variable=False
)
print(f"Challenge length: {len(challenge)}")
print(f"Variable response length: {len(var_response)}")
print(f"Fixed response length: {len(fixed_response)}")Some YubiKey configurations require user button press for challenge-response.
import yubico
yk = yubico.find_yubikey()
challenge = b"authentication challenge"
# Allow blocking for button press
try:
response = yk.challenge_response(
challenge=challenge,
mode='HMAC',
slot=1,
may_block=True
)
print("Challenge-response successful")
except yubico.yubikey_base.YubiKeyTimeout:
print("Timed out waiting for button press")
# Fail immediately if button press required
try:
response = yk.challenge_response(
challenge=challenge,
mode='HMAC',
slot=1,
may_block=False
)
print("Challenge-response successful (no button press required)")
except yubico.yubikey_base.YubiKeyTimeout:
print("Button press required but may_block=False")YubiKeys can have different challenge-response configurations in each slot.
import yubico
yk = yubico.find_yubikey()
challenge = b"test challenge"
# Test both slots
for slot in [1, 2]:
try:
response = yk.challenge_response(challenge, slot=slot)
print(f"Slot {slot} response: {response.hex()}")
except yubico.yubikey_base.YubiKeyError:
print(f"Slot {slot} not configured for challenge-response")Complete authentication workflow using challenge-response.
import yubico
import hashlib
import hmac
import os
import time
def yubikey_authenticate(user_id, expected_secret=None):
"""
Authenticate user using YubiKey challenge-response.
Parameters:
- user_id (str): User identifier
- expected_secret (bytes): Known secret for verification (demo only)
Returns:
bool: Authentication success
"""
try:
# Connect to YubiKey
yk = yubico.find_yubikey()
# Generate random challenge
challenge = os.urandom(20)
# Get YubiKey response
yk_response = yk.challenge_response(
challenge=challenge,
mode='HMAC',
slot=1,
variable=False # Fixed 20-byte response
)
# For demo: verify against known secret
if expected_secret:
expected_response = hmac.new(
expected_secret,
challenge,
hashlib.sha1
).digest()
if hmac.compare_digest(yk_response, expected_response):
print(f"Authentication successful for user: {user_id}")
return True
else:
print(f"Authentication failed for user: {user_id}")
return False
# In real applications, verify response against stored hash
print(f"Challenge-response completed for user: {user_id}")
print(f"Response: {yk_response.hex()}")
return True
except yubico.yubico_exception.YubicoError as e:
print(f"YubiKey authentication failed: {e.reason}")
return False
# Example usage
secret = os.urandom(20) # In practice, store this securely
success = yubikey_authenticate("alice", secret)Challenge-response operations have different performance characteristics.
import yubico
import time
yk = yubico.find_yubikey()
challenge = b"performance test"
# Measure response time
start_time = time.time()
response = yk.challenge_response(challenge, mode='HMAC', slot=1)
end_time = time.time()
print(f"Challenge-response time: {(end_time - start_time)*1000:.2f} ms")
# Test multiple operations
times = []
for i in range(10):
start = time.time()
yk.challenge_response(f"test{i}".encode(), mode='HMAC', slot=1)
end = time.time()
times.append((end - start) * 1000)
avg_time = sum(times) / len(times)
print(f"Average response time over 10 operations: {avg_time:.2f} ms")Comprehensive error handling for challenge-response operations.
import yubico
def safe_challenge_response(yk, challenge, **kwargs):
"""
Perform challenge-response with comprehensive error handling.
"""
try:
return yk.challenge_response(challenge, **kwargs)
except yubico.yubikey_base.YubiKeyVersionError:
print("ERROR: YubiKey doesn't support challenge-response")
print("Required: YubiKey 2.2+ with challenge-response configuration")
except yubico.yubikey_base.YubiKeyTimeout:
print("ERROR: Operation timed out")
print("This may indicate button press is required")
except yubico.yubico_exception.InputError as e:
print(f"ERROR: Invalid challenge data: {e.reason}")
print("HMAC mode: up to 64 bytes")
print("OTP mode: exactly 6 bytes")
except yubico.yubikey_base.YubiKeyError as e:
print(f"ERROR: Challenge-response failed: {e.reason}")
print("Check that the specified slot is configured for challenge-response")
return None
# Usage
yk = yubico.find_yubikey()
challenge = b"test challenge"
response = safe_challenge_response(yk, challenge, mode='HMAC', slot=1)
if response:
print(f"Success: {response.hex()}")
else:
print("Challenge-response failed")Before using challenge-response, the YubiKey must be configured appropriately:
import yubico
from yubico.yubikey_config import YubiKeyConfig
import os
# Configure YubiKey for HMAC challenge-response
yk = yubico.find_yubikey()
cfg = yk.init_config()
# Set secret key (20 bytes for HMAC)
secret = os.urandom(20)
cfg.mode_challenge_response(
secret=secret,
type='HMAC',
variable=True,
require_button=False # Set to True if button press required
)
# Write configuration to slot 1
yk.write_config(cfg, slot=1)
print("YubiKey configured for challenge-response")
# Test the configuration
test_challenge = b"configuration test"
response = yk.challenge_response(test_challenge, slot=1)
print(f"Test response: {response.hex()}")Install with Tessl CLI
npx tessl i tessl/pypi-python-yubico