CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-python-yubico

Python library for communicating with Yubico YubiKey hardware authentication tokens

Pending
Overview
Eval results
Files

challenge-response.mddocs/

Challenge-Response Operations

HMAC-SHA1 and Yubico challenge-response authentication for secure authentication workflows, supporting both variable and fixed-length responses with configurable user interaction requirements.

Capabilities

Challenge-Response Authentication

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 Challenge-Response

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

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")

Variable vs Fixed Length Responses

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)}")

Button Press Requirements

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")

Multi-Slot Challenge-Response

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")

Authentication Workflow Example

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)

Performance Considerations

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")

Error Handling

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")

Configuration Requirements

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

docs

challenge-response.md

configuration.md

device-discovery.md

device-interface.md

exceptions.md

index.md

utilities.md

tile.json