blob: ffff641ea503635fc1f5d30e03c077c0963345de [file] [log] [blame]
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import random
import stat
import string
import sys
import tempfile
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import xmlrpc_types
def deserialize(serialized):
"""Deserialize a SecurityConfig.
@param serialized dict representing a serialized SecurityConfig.
@return a SecurityConfig object built from |serialized|.
"""
return xmlrpc_types.deserialize(serialized, module=sys.modules[__name__])
class SecurityConfig(xmlrpc_types.XmlRpcStruct):
"""Abstracts the security configuration for a WiFi network.
This bundle of credentials can be passed to both HostapConfig and
AssociationParameters so that both shill and hostapd can set up and connect
to an encrypted WiFi network. By default, we'll assume we're connecting
to an open network.
"""
SERVICE_PROPERTY_PASSPHRASE = 'Passphrase'
SERVICE_PROPERTY_FT_ENABLED = 'WiFi.FTEnabled'
def __init__(self, security='none'):
super(SecurityConfig, self).__init__()
self.security = security
def get_hostapd_config(self):
"""@return dict fragment of hostapd configuration for security."""
return {}
def get_shill_service_properties(self):
"""@return dict of shill service properties."""
return {}
def get_wpa_cli_properties(self):
"""@return dict values to be set with wpa_cli set_network."""
return {'key_mgmt': 'NONE'}
def install_router_credentials(self, host):
"""Install the necessary credentials on the router.
@param host host object representing the router.
"""
pass # Many authentication methods have no special router credentials.
def install_client_credentials(self, tpm_store):
"""Install credentials on the local host (hopefully a DUT).
Only call this if we're running on a DUT in a WiFi test. This
method can do things like install credentials into the TPM.
@param tpm_store TPMStore object representing the TPM on our DUT.
"""
pass # Many authentication methods have no special client credentials.
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
', '.join(['%s=%r' % item
for item in vars(self).iteritems()]))
class WEPConfig(SecurityConfig):
"""Abstracts security configuration for a WiFi network using static WEP."""
# Open system authentication means that we don't do a 4 way AUTH handshake,
# and simply start using the WEP keys after association finishes.
AUTH_ALGORITHM_OPEN = 1
# This refers to a mode where the AP sends a plaintext challenge and the
# client sends back the challenge encrypted with the WEP key as part of a 4
# part auth handshake.
AUTH_ALGORITHM_SHARED = 2
AUTH_ALGORITHM_DEFAULT = AUTH_ALGORITHM_OPEN
@staticmethod
def _format_key(key, ascii_key_formatter):
"""Returns a key formatted to for its appropriate consumer.
Both hostapd and wpa_cli want their ASCII encoded WEP keys formatted
in a particular way. Hex string on the other hand can be given raw.
Other key formats aren't even accepted, and this method will raise
and exception if it sees such a key.
@param key string a 40/104 bit WEP key.
@param ascii_key_formatter converter function that escapes a WEP
string-encoded passphrase. This conversion varies in format
depending on the consumer.
@return string corrected formatted WEP key.
"""
if len(key) in (5, 13):
# These are 'ASCII' strings, or at least N-byte strings
# of the right size.
return ascii_key_formatter(key)
if len(key) in (10, 26):
# These are hex encoded byte strings.
return key
raise error.TestFail('Invalid WEP key: %r' % key)
def __init__(self, wep_keys, wep_default_key=0,
auth_algorithm=AUTH_ALGORITHM_DEFAULT):
"""Construct a WEPConfig object.
@param wep_keys list of string WEP keys.
@param wep_default_key int 0 based index into |wep_keys| for the default
key.
@param auth_algorithm int bitfield of AUTH_ALGORITHM_* defined above.
"""
super(WEPConfig, self).__init__(security='wep')
self.wep_keys = wep_keys
self.wep_default_key = wep_default_key
self.auth_algorithm = auth_algorithm
if self.auth_algorithm & ~(self.AUTH_ALGORITHM_OPEN |
self.AUTH_ALGORITHM_SHARED):
raise error.TestFail('Invalid authentication mode specified (%d).' %
self.auth_algorithm)
if self.wep_keys and len(self.wep_keys) > 4:
raise error.TestFail('More than 4 WEP keys specified (%d).' %
len(self.wep_keys))
def get_hostapd_config(self):
"""@return dict fragment of hostapd configuration for security."""
ret = {}
quote = lambda x: '"%s"' % x
for idx,key in enumerate(self.wep_keys):
ret['wep_key%d' % idx] = self._format_key(key, quote)
ret['wep_default_key'] = self.wep_default_key
ret['auth_algs'] = self.auth_algorithm
return ret
def get_shill_service_properties(self):
"""@return dict of shill service properties."""
return {self.SERVICE_PROPERTY_PASSPHRASE: '%d:%s' % (
self.wep_default_key,
self.wep_keys[self.wep_default_key])}
def get_wpa_cli_properties(self):
properties = super(WEPConfig, self).get_wpa_cli_properties()
quote = lambda x: '\\"%s\\"' % x
for idx, key in enumerate(self.wep_keys):
properties['wep_key%d' % idx] = self._format_key(key, quote)
properties['wep_tx_keyidx'] = self.wep_default_key
if self.auth_algorithm == self.AUTH_ALGORITHM_SHARED:
properties['auth_alg'] = 'SHARED'
return properties
class WPAConfig(SecurityConfig):
"""Abstracts security configuration for a WPA encrypted WiFi network."""
# We have the option of turning on WPA, WPA2, or both via a bitfield.
MODE_PURE_WPA = 1
MODE_PURE_WPA2 = 2
MODE_MIXED_WPA = MODE_PURE_WPA | MODE_PURE_WPA2
MODE_DEFAULT = MODE_MIXED_WPA
# WPA2 mandates the use of AES in CCMP mode.
# WPA allows the use of 'ordinary' AES, but mandates support for TKIP.
# The protocol however seems to indicate that you just list a bunch of
# different ciphers that you support and we'll start speaking one.
CIPHER_CCMP = 'CCMP'
CIPHER_TKIP = 'TKIP'
def __init__(self, psk='', wpa_mode=MODE_DEFAULT, wpa_ciphers=[],
wpa2_ciphers=[], wpa_ptk_rekey_period=None,
wpa_gtk_rekey_period=None, wpa_gmk_rekey_period=None,
use_strict_rekey=None, use_ft=None):
"""Construct a WPAConfig.
@param psk string a passphrase (64 hex characters or an ASCII phrase up
to 63 characters long).
@param wpa_mode int one of MODE_* above.
@param wpa_ciphers list of ciphers to advertise in the WPA IE.
@param wpa2_ciphers list of ciphers to advertise in the WPA2 IE.
hostapd will fall back on WPA ciphers for WPA2 if this is
left unpopulated.
@param wpa_ptk_rekey_period int number of seconds between PTK rekeys.
@param wpa_gtk_rekey_period int number of second between GTK rekeys.
@param wpa_gmk_rekey_period int number of seconds between GMK rekeys.
The GMK is a key internal to hostapd used to generate GTK.
It is the 'master' key.
@param use_strict_rekey bool True iff hostapd should refresh the GTK
whenever any client leaves the group.
"""
super(WPAConfig, self).__init__(security='psk')
self.psk = psk
self.wpa_mode = wpa_mode
self.wpa_ciphers = wpa_ciphers
self.wpa2_ciphers = wpa2_ciphers
self.wpa_ptk_rekey_period = wpa_ptk_rekey_period
self.wpa_gtk_rekey_period = wpa_gtk_rekey_period
self.wpa_gmk_rekey_period = wpa_gmk_rekey_period
self.use_strict_rekey = use_strict_rekey
self.use_ft = use_ft
if len(psk) > 64:
raise error.TestFail('WPA passphrases can be no longer than 63 '
'characters (or 64 hex digits).')
if len(psk) == 64:
for c in psk:
if c not in '0123456789abcdefABCDEF':
raise error.TestFail('Invalid PMK: %r' % psk)
def get_hostapd_config(self):
"""@return dict fragment of hostapd configuration for security."""
if not self.wpa_mode:
raise error.TestFail('Cannot configure WPA unless we know which '
'mode to use.')
if self.MODE_PURE_WPA & self.wpa_mode and not self.wpa_ciphers:
raise error.TestFail('Cannot configure WPA unless we know which '
'ciphers to use.')
if not self.wpa_ciphers and not self.wpa2_ciphers:
raise error.TestFail('Cannot configure WPA2 unless we have some '
'ciphers.')
ret = {'wpa': self.wpa_mode,
'wpa_key_mgmt': 'WPA-PSK'}
if self.use_ft:
ret['wpa_key_mgmt'] = 'FT-PSK'
if len(self.psk) == 64:
ret['wpa_psk'] = self.psk
else:
ret['wpa_passphrase'] = self.psk
if self.wpa_ciphers:
ret['wpa_pairwise'] = ' '.join(self.wpa_ciphers)
if self.wpa2_ciphers:
ret['rsn_pairwise'] = ' '.join(self.wpa2_ciphers)
if self.wpa_ptk_rekey_period:
ret['wpa_ptk_rekey'] = self.wpa_ptk_rekey_period
if self.wpa_gtk_rekey_period:
ret['wpa_group_rekey'] = self.wpa_gtk_rekey_period
if self.wpa_gmk_rekey_period:
ret['wpa_gmk_rekey'] = self.wpa_gmk_rekey_period
if self.use_strict_rekey:
ret['wpa_strict_rekey'] = 1
return ret
def get_shill_service_properties(self):
"""@return dict of shill service properties."""
ret = {self.SERVICE_PROPERTY_PASSPHRASE: self.psk}
if self.use_ft is not None:
ret[self.SERVICE_PROPERTY_FT_ENABLED] = self.use_ft
return ret
def get_wpa_cli_properties(self):
properties = super(WPAConfig, self).get_wpa_cli_properties()
# TODO(wiley) This probably doesn't work for raw PMK.
protos = []
if self.wpa_mode & self.MODE_PURE_WPA:
protos.append('WPA')
if self.wpa_mode & self.MODE_PURE_WPA2:
protos.append('RSN')
properties.update({'psk': '\\"%s\\"' % self.psk,
'key_mgmt': 'WPA-PSK',
'proto': ' '.join(protos)})
if self.use_ft:
properties['key_mgmt'] = 'FT-PSK'
return properties
class EAPConfig(SecurityConfig):
"""Abstract superclass that implements certificate/key installation."""
DEFAULT_EAP_USERS = '* TLS'
DEFAULT_EAP_IDENTITY = 'chromeos'
SERVICE_PROPERTY_CA_CERT_PEM = 'EAP.CACertPEM'
SERVICE_PROPERTY_CLIENT_CERT_ID = 'EAP.CertID'
SERVICE_PROPERTY_EAP_IDENTITY = 'EAP.Identity'
SERVICE_PROPERTY_EAP_KEY_MGMT = 'EAP.KeyMgmt'
SERVICE_PROPERTY_EAP_PASSWORD = 'EAP.Password'
SERVICE_PROPERTY_EAP_PIN = 'EAP.PIN'
SERVICE_PROPERTY_INNER_EAP= 'EAP.InnerEAP'
SERVICE_PROPERTY_PRIVATE_KEY_ID = 'EAP.KeyID'
SERVICE_PROPERTY_USE_SYSTEM_CAS = 'EAP.UseSystemCAs'
last_tpm_id = 8800
@staticmethod
def reserve_TPM_id():
"""@return session unique TPM identifier."""
ret = str(EAPConfig.last_tpm_id)
EAPConfig.last_tpm_id += 1
return ret
def __init__(self, security='802_1x', file_suffix=None, use_system_cas=None,
server_ca_cert=None, server_cert=None, server_key=None,
server_eap_users=None,
client_ca_cert=None, client_cert=None, client_key=None,
client_cert_id=None, client_key_id=None,
eap_identity=None, use_ft=None):
"""Construct an EAPConfig.
@param file_suffix string unique file suffix on DUT.
@param use_system_cas False iff we should ignore server certificates.
@param server_ca_cert string PEM encoded CA certificate for the server.
@param server_cert string PEM encoded identity certificate for server.
@param server_key string PEM encoded private key for server.
@param server_eap_users string contents of EAP user file.
@param client_ca_cert string PEM encoded CA certificate for client.
@param client_cert string PEM encoded identity certificate for client.
@param client_key string PEM encoded private key for client.
@param client_cert_id string identifier for client certificate in TPM.
@param client_key_id string identifier for client private key in TPM.
@param eap_identity string user to authenticate as during EAP.
"""
super(EAPConfig, self).__init__(security=security)
self.use_system_cas = use_system_cas
self.server_ca_cert = server_ca_cert
self.server_cert = server_cert
self.server_key = server_key
self.server_eap_users = server_eap_users or self.DEFAULT_EAP_USERS
self.client_ca_cert = client_ca_cert
self.client_cert = client_cert
self.client_key = client_key
if file_suffix is None:
suffix_letters = string.ascii_lowercase + string.digits
file_suffix = ''.join(random.choice(suffix_letters)
for x in range(10))
logging.debug('Choosing unique file_suffix %s.', file_suffix)
self.server_ca_cert_file = '/tmp/hostapd_ca_cert_file.' + file_suffix
self.server_cert_file = '/tmp/hostapd_cert_file.' + file_suffix
self.server_key_file = '/tmp/hostapd_key_file.' + file_suffix
self.server_eap_user_file = '/tmp/hostapd_eap_user_file.' + file_suffix
# While these paths won't make it across the network, the suffix will.
self.file_suffix = file_suffix
self.client_cert_id = client_cert_id or self.reserve_TPM_id()
self.client_key_id = client_key_id or self.reserve_TPM_id()
# This gets filled in at install time.
self.pin = None
# The slot where the certificate/key are installed in the TPM.
self.client_cert_slot_id = None
self.client_key_slot_id = None
self.eap_identity = eap_identity or self.DEFAULT_EAP_IDENTITY
self.use_ft = use_ft
def install_router_credentials(self, host):
"""Install the necessary credentials on the router.
@param host host object representing the router.
"""
files = [(self.server_ca_cert, self.server_ca_cert_file),
(self.server_cert, self.server_cert_file),
(self.server_key, self.server_key_file),
(self.server_eap_users, self.server_eap_user_file)]
for content, path in files:
# If we omit a parameter, just omit copying a file over.
if content is None:
continue
# Write the contents to local disk first so we can use the easy
# built in mechanism to do this.
with tempfile.NamedTemporaryFile() as f:
f.write(content)
f.flush()
os.chmod(f.name, stat.S_IRUSR | stat.S_IWUSR |
stat.S_IRGRP | stat.S_IWGRP |
stat.S_IROTH | stat.S_IWOTH)
host.send_file(f.name, path, delete_dest=True)
def install_client_credentials(self, tpm_store):
"""Install credentials on the local host (hopefully a DUT).
Only call this if we're running on a DUT in a WiFi test. This
method can do things like install credentials into the TPM.
@param tpm_store TPMStore object representing the TPM on our DUT.
"""
if self.client_cert:
tpm_store.install_certificate(self.client_cert, self.client_cert_id)
self.client_cert_slot_id = tpm_store.SLOT_ID
self.pin = tpm_store.PIN
if self.client_key:
tpm_store.install_private_key(self.client_key, self.client_key_id)
self.client_key_slot_id = tpm_store.SLOT_ID
self.pin = tpm_store.PIN
def get_shill_service_properties(self):
"""@return dict of shill service properties."""
ret = {self.SERVICE_PROPERTY_EAP_IDENTITY: self.eap_identity}
if self.pin:
ret[self.SERVICE_PROPERTY_EAP_PIN] = self.pin
if self.client_ca_cert:
# Technically, we could accept a list of certificates here, but we
# have no such tests.
ret[self.SERVICE_PROPERTY_CA_CERT_PEM] = [self.client_ca_cert]
if self.client_cert:
ret[self.SERVICE_PROPERTY_CLIENT_CERT_ID] = (
'%s:%s' % (self.client_cert_slot_id, self.client_cert_id))
if self.client_key:
ret[self.SERVICE_PROPERTY_PRIVATE_KEY_ID] = (
'%s:%s' % (self.client_key_slot_id, self.client_key_id))
if self.use_system_cas is not None:
ret[self.SERVICE_PROPERTY_USE_SYSTEM_CAS] = self.use_system_cas
if self.use_ft is not None:
ret[self.SERVICE_PROPERTY_FT_ENABLED] = self.use_ft
return ret
def get_hostapd_config(self):
"""@return dict fragment of hostapd configuration for security."""
return {'ieee8021x': 1, # Enable 802.1x support.
'eap_server' : 1, # Do EAP inside hostapd to avoid RADIUS.
'ca_cert': self.server_ca_cert_file,
'server_cert': self.server_cert_file,
'private_key': self.server_key_file,
'eap_user_file': self.server_eap_user_file}
class DynamicWEPConfig(EAPConfig):
"""Configuration settings bundle for dynamic WEP.
This is a WEP encrypted connection where the keys are negotiated after the
client authenticates via 802.1x.
"""
DEFAULT_REKEY_PERIOD = 20
def __init__(self, use_short_keys=False,
wep_rekey_period=DEFAULT_REKEY_PERIOD,
server_ca_cert=None, server_cert=None, server_key=None,
client_ca_cert=None, client_cert=None, client_key=None,
file_suffix=None, client_cert_id=None, client_key_id=None):
"""Construct a DynamicWEPConfig.
@param use_short_keys bool force hostapd to use 40 bit WEP keys.
@param wep_rekey_period int number of second between rekeys.
@param server_ca_cert string PEM encoded CA certificate for the server.
@param server_cert string PEM encoded identity certificate for server.
@param server_key string PEM encoded private key for server.
@param client_ca_cert string PEM encoded CA certificate for client.
@param client_cert string PEM encoded identity certificate for client.
@param client_key string PEM encoded private key for client.
@param file_suffix string unique file suffix on DUT.
@param client_cert_id string identifier for client certificate in TPM.
@param client_key_id string identifier for client private key in TPM.
"""
super(DynamicWEPConfig, self).__init__(
security='wep', file_suffix=file_suffix,
server_ca_cert=server_ca_cert, server_cert=server_cert,
server_key=server_key, client_ca_cert=client_ca_cert,
client_cert=client_cert, client_key=client_key,
client_cert_id=client_cert_id, client_key_id=client_key_id)
self.use_short_keys = use_short_keys
self.wep_rekey_period = wep_rekey_period
def get_hostapd_config(self):
"""@return dict fragment of hostapd configuration for security."""
ret = super(DynamicWEPConfig, self).get_hostapd_config()
key_len = 13 # 128 bit WEP, 104 secret bits.
if self.use_short_keys:
key_len = 5 # 64 bit WEP, 40 bits of secret.
ret.update({'wep_key_len_broadcast': key_len,
'wep_key_len_unicast': key_len,
'wep_rekey_period': self.wep_rekey_period})
return ret
def get_shill_service_properties(self):
"""@return dict of shill service properties."""
ret = super(DynamicWEPConfig, self).get_shill_service_properties()
ret.update({self.SERVICE_PROPERTY_EAP_KEY_MGMT: 'IEEE8021X'})
return ret
class WPAEAPConfig(EAPConfig):
"""Security type to set up a WPA tunnel via EAP-TLS negotiation."""
def __init__(self, file_suffix=None, use_system_cas=None,
server_ca_cert=None, server_cert=None, server_key=None,
client_ca_cert=None, client_cert=None, client_key=None,
client_cert_id=None, client_key_id=None,
eap_identity=None, server_eap_users=None,
wpa_mode=WPAConfig.MODE_PURE_WPA, use_ft=None):
"""Construct a DynamicWEPConfig.
@param file_suffix string unique file suffix on DUT.
@param use_system_cas False iff we should ignore server certificates.
@param server_ca_cert string PEM encoded CA certificate for the server.
@param server_cert string PEM encoded identity certificate for server.
@param server_key string PEM encoded private key for server.
@param client_ca_cert string PEM encoded CA certificate for client.
@param client_cert string PEM encoded identity certificate for client.
@param client_key string PEM encoded private key for client.
@param client_cert_id string identifier for client certificate in TPM.
@param client_key_id string identifier for client private key in TPM.
@param eap_identity string user to authenticate as during EAP.
@param server_eap_users string contents of server EAP users file.
"""
super(WPAEAPConfig, self).__init__(
file_suffix=file_suffix, use_system_cas=use_system_cas,
server_ca_cert=server_ca_cert, server_cert=server_cert,
server_key=server_key, client_ca_cert=client_ca_cert,
client_cert=client_cert, client_key=client_key,
client_cert_id=client_cert_id, client_key_id=client_key_id,
eap_identity=eap_identity, server_eap_users=server_eap_users,
use_ft=use_ft)
self.wpa_mode = wpa_mode
def get_hostapd_config(self):
"""@return dict fragment of hostapd configuration for security."""
ret = super(WPAEAPConfig, self).get_hostapd_config()
# If we wanted to expand test coverage to WPA2/PEAP combinations
# or particular ciphers, we'd have to let people set these
# settings manually. But for now, do the simple thing.
ret.update({'wpa': self.wpa_mode,
'wpa_pairwise': WPAConfig.CIPHER_CCMP,
'wpa_key_mgmt':'WPA-EAP'})
if self.use_ft:
ret['wpa_key_mgmt'] = 'FT-EAP'
return ret
class Tunneled1xConfig(WPAEAPConfig):
"""Security type to set up a TTLS/PEAP connection.
Both PEAP and TTLS are tunneled protocols which use EAP inside of a TLS
secured tunnel. The secured tunnel is a symmetric key encryption scheme
negotiated under the protection of a public key in the server certificate.
Thus, we'll see server credentials in the form of certificates, but client
credentials in the form of passwords and a CA Cert to root the trust chain.
"""
TTLS_PREFIX = 'TTLS-'
LAYER1_TYPE_PEAP = 'PEAP'
LAYER1_TYPE_TTLS = 'TTLS'
LAYER2_TYPE_GTC = 'GTC'
LAYER2_TYPE_MSCHAPV2 = 'MSCHAPV2'
LAYER2_TYPE_MD5 = 'MD5'
LAYER2_TYPE_TTLS_MSCHAPV2 = TTLS_PREFIX + 'MSCHAPV2'
LAYER2_TYPE_TTLS_MSCHAP = TTLS_PREFIX + 'MSCHAP'
LAYER2_TYPE_TTLS_PAP = TTLS_PREFIX + 'PAP'
def __init__(self, server_ca_cert, server_cert, server_key,
client_ca_cert, eap_identity, password,
outer_protocol=LAYER1_TYPE_PEAP,
inner_protocol=LAYER2_TYPE_MD5,
client_password=None, file_suffix=None):
self.password = password
if client_password is not None:
# Override the password used on the client. This lets us set
# bad passwords for testing. However, we use the real password
# below for the server config.
self.password = client_password
self.inner_protocol = inner_protocol
# hostapd wants these surrounded in double quotes.
quote = lambda x: '"' + x + '"'
eap_users = map(' '.join, [('*', outer_protocol),
(quote(eap_identity), inner_protocol,
quote(password), '[2]')])
super(Tunneled1xConfig, self).__init__(
server_ca_cert=server_ca_cert,
server_cert=server_cert,
server_key=server_key,
server_eap_users='\n'.join(eap_users),
client_ca_cert=client_ca_cert,
eap_identity=eap_identity,
file_suffix=file_suffix)
def get_shill_service_properties(self):
"""@return dict of shill service properties."""
ret = super(Tunneled1xConfig, self).get_shill_service_properties()
ret.update({self.SERVICE_PROPERTY_EAP_PASSWORD: self.password})
if self.inner_protocol.startswith(self.TTLS_PREFIX):
auth_str = 'auth=' + self.inner_protocol[len(self.TTLS_PREFIX):]
ret.update({self.SERVICE_PROPERTY_INNER_EAP: auth_str})
return ret