devices/eap_config/DeviceXML.php
<?php
/*
* *****************************************************************************
* Contributions to this work were made on behalf of the GÉANT project, a
* project that has received funding from the European Union’s Framework
* Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
* Horizon 2020 research and innovation programme under Grant Agreements No.
* 691567 (GN4-1) and No. 731122 (GN4-2).
* On behalf of the aforementioned projects, GEANT Association is the sole owner
* of the copyright in all material which was developed by a member of the GÉANT
* project. GÉANT Vereniging (Association) is registered with the Chamber of
* Commerce in Amsterdam with registration number 40535155 and operates in the
* UK as a branch of GÉANT Vereniging.
*
* Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands.
* UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
*
* License: see the web/copyright.inc.php file in the file structure or
* <base_url>/copyright.php after deploying the software
*/
/**
* This file defines an abstract class used for generic XML
* devices
* actual modules only define available EAP types.
*
* @author Maja Gorecka-Wolniewicz <mgw@umk.pl>
* @author Tomasz Wolniewicz <twoln@umk.pl>
*
* @package ModuleWriting
*/
namespace devices\eap_config;
use Exception;
/**
* This class implements full functionality of the generic XML device
* the only function of the extensions of this class is to specify
* supported EAP methods.
* Instead of specifying supported EAPS an extension can set $all_eaps to true
* this will cause the installer to configure all EAP methods supported by
* the current profile and declared by the given device.
*/
abstract class DeviceXML extends \core\DeviceConfig
{
/**
* @var array $AuthMethodElements is used to limit
* XML elements present within ServerSideCredentials and
* ClientSideCredentials to ones which are relevant
* for a given EAP method.
* @var array of XLM element names which are allowed
* EAP method names are defined in core/EAP.php
*/
private $authMethodElements = [
'server' => [
\core\common\EAP::TLS => ['CA', 'ServerID'],
\core\common\EAP::FAST => ['CA', 'ServerID'],
\core\common\EAP::PEAP => ['CA', 'ServerID'],
\core\common\EAP::TTLS => ['CA', 'ServerID'],
\core\common\EAP::PWD => ['ServerID'],
],
'client' => [
\core\common\EAP::TLS => ['UserName', 'Password', 'ClientCertificate'],
\core\common\EAP::NE_MSCHAP2 => ['UserName', 'Password', 'OuterIdentity', 'InnerIdentitySuffix', 'InnerIdentityHint'],
\core\common\EAP::MSCHAP2 => ['UserName', 'Password', 'OuterIdentity', 'InnerIdentitySuffix', 'InnerIdentityHint'],
\core\common\EAP::GTC => ['UserName', 'OneTimeToken'],
\core\common\EAP::NE_PAP => ['UserName', 'Password', 'OuterIdentity', 'InnerIdentitySuffix', 'InnerIdentityHint'],
\core\common\EAP::NE_SILVERBULLET => ['UserName', 'ClientCertificate', 'OuterIdentity'],
\core\common\EAP::NONE => [],
]
];
/**
* construct the device
*/
public function __construct()
{
parent::__construct();
}
/**
* $langScope can be 'global' when all lang and all lang-specific information
* is dumped or 'single' when only the selected lang (and defaults) are passed
* NOTICE: 'global' is not yet supported
*
* @var string
*/
public $langScope;
/**
* whether all EAP types should be included in the file or only the
* preferred one
*
* @var boolean
*/
public $allEaps = false;
/**
* vendor-specific additional information, this is nit yest fully
* implemented due to lack of use cases.
*
* @var array
*/
public $VendorSpecific;
/**
* A flag to preserve backwards compatibility for eduroamCAT application
*/
public $eduroamCATcompatibility = false;
public $singleEAPProvider = false;
private $eapId;
private $namespace;
private $providerInfo;
private $authMethodsList;
/**
* create HTML code explaining the installer
*
* @return string
*/
public function writeDeviceInfo()
{
\core\common\Entity::intoThePotatoes();
$out = "<p>";
$out .= sprintf(_("This is a generic configuration file in the IETF <a href='%s'>EAP Metadata -00</a> XML format."), "https://tools.ietf.org/html/draft-winter-opsawg-eap-metadata-00");
\core\common\Entity::outOfThePotatoes();
return $out;
}
/**
* create the actual XML file
*
* @return string filename of the generated installer
* @throws Exception
*
*/
public function writeInstaller()
{
\core\common\Entity::intoThePotatoes();
$rootname = 'EAPIdentityProviderList';
$dom = new \DOMDocument('1.0', 'utf-8');
$root = $dom->createElement($rootname);
$dom->appendChild($root);
$ns = $dom->createAttributeNS( 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation' );
$ns->value = "eap-metadata.xsd";
$root->appendChild($ns);
$this->openRoamingToU = sprintf(_("I have read and agree to OpenRoaming Terms of Use at %s."), "https://wballiance.com/openroaming/toc-2020/");
foreach ($this->languageInstance->getAllTranslations("I have read and agree to OpenRoaming Terms of Use at %s", "device") as $lang => $message) {
$this->openRoamingToUArray[$lang] = sprintf($message, "https://wballiance.com/openroaming/toc-2020/");
}
if (empty($this->attributes['internal:realm'][0])) {
$this->eapId = 'undefined';
$this->namespace = 'urn:undefined';
} else {
$this->eapId = $this->attributes['internal:realm'][0];
$this->namespace = 'urn:RFC4282:realm';
}
$this->authMethodsList = $this->getAuthMethodsList();
$this->loggerInstance->debug(5, $this->attributes['internal:networks'], "NETWORKS:", "\n");
/*
* This approach is forced by geteduroam compatibility. We pack all networks into a single Provider
* with the exception of the openroaming one which we pack separately.
*/
if ($this->singleEAPProvider === true) {
/*
* if "condition" is set to openroaming, OpenRoaming terms of use must be mentioned
* unless the preagreed is set for openroaming
* if "ask" is set then we generate a separate OR profile which needs to contain the OR ToU
* the ToU is not needed in the eduroam-only profile
*/
$ssids = [];
$ois = [];
$orNetwork = [];
foreach ($this->attributes['internal:networks'] as $netName => $netDefinition) {
if ($netDefinition['condition'] === 'internal:openroaming' &&
$this->attributes['internal:openroaming']) {
$this->setORtou();
if (preg_match("/^ask/",$this->attributes['media:openroaming'][0])) {
$orNetwork = $netDefinition;
continue;
}
}
foreach ($netDefinition['ssid'] as $ssid) {
$ssids[] = $ssid;
}
foreach ($netDefinition['oi'] as $oi) {
$ois[] = $oi;
}
}
if (!empty($orNetwork)) {
$this->addORtou = false;
}
$this->providerInfo = $this->getProviderInfo();
if (!empty($ssids) || !empty($ois)) {
\core\DeviceXMLmain::marshalObject($dom, $root, 'EAPIdentityProvider', $this->eapIdp($ssids, $ois));
}
if (!empty($orNetwork)) {
// here we need to add the Tou unless preagreed is set
$this->setORtou();
$this->providerInfo = $this->getProviderInfo();
\core\DeviceXMLmain::marshalObject($dom, $root, 'EAPIdentityProvider', $this->eapIdp($orNetwork['ssid'], $orNetwork['oi']));
}
} else {
foreach ($this->attributes['internal:networks'] as $netName => $netDefinition) {
if ($netDefinition['condition'] === 'internal:openroaming' &&
$this->attributes['internal:openroaming']) {
$this->setORtou();
} else {
$this->addORtou = false;
}
$this->providerInfo = $this->getProviderInfo();
$ssids = $netDefinition['ssid'];
$ois = $netDefinition['oi'];
if (!empty($ssids) || !empty($ois)) {
\core\DeviceXMLmain::marshalObject($dom, $root, 'EAPIdentityProvider', $this->eapIdp($ssids, $ois));
}
}
}
if ($dom->schemaValidate(ROOT.'/devices/eap_config/eap-metadata.xsd') === FALSE) {
throw new Exception("Schema validation failed for eap-metadata");
}
$dom->formatOutput = true;
file_put_contents($this->installerBasename.'.eap-config', $dom->saveXML($dom));
\core\common\Entity::outOfThePotatoes();
return($this->installerBasename.'.eap-config');
}
private function setORtou() {
if (preg_match("/preagreed/",$this->attributes['media:openroaming'][0])) {
$this->addORtou = false;
} else {
$this->addORtou = true;
}
}
/**
* determines the inner authentication. Is it EAP, and which mechanism is used to convey actual auth data
* @param array $eap the EAP type for which we want to get the inner auth
* @return array
*/
private function eapIdp($ssids, $oids)
{
$eapIdp = new \core\DeviceXMLmain();
$eapIdp->setAttribute('version', '1');
if ($this->langScope === 'single') {
$eapIdp->setAttribute('lang', $this->languageInstance->getLang());
}
$eapIdp->setAttribute('ID', $this->eapId);
$eapIdp->setAttribute('namespace', $this->namespace);
$authMethods = new \core\DeviceXMLmain();
$authMethods->setChild('AuthenticationMethod', $this->authMethodsList);
$eapIdp->setChild('AuthenticationMethods', $authMethods);
$eapIdp->setChild('CredentialApplicability', $this->getCredentialApplicability($ssids,$oids));
// TODO $eap_idp->setChild('ValidUntil',$this->getValidUntil());
$eapIdp->setChild('ProviderInfo', $this->providerInfo);
// TODO $eap_idp->setChild('VendorSpecific',$this->getVendorSpecific());
return($eapIdp);
}
/**
* determines the inner authentication. Is it EAP, and which mechanism is used to convey actual auth data
* @param array $eap the EAP type for which we want to get the inner auth
* @return array
*/
private function innerAuth($eap)
{
$out = [];
$out['EAP'] = 0;
switch ($eap["INNER"]) {
case \core\common\EAP::NE_MSCHAP2:
if ($this->eduroamCATcompatibility === TRUE) {
$out['METHOD'] = \core\common\EAP::MSCHAP2;
$out['EAP'] = 1;
} else {
$out['METHOD'] = $eap["INNER"];
}
break;
case \core\common\EAP::NE_SILVERBULLET:
$out['METHOD'] = \core\common\EAP::NONE;
break;
default:
$out['METHOD'] = $eap["INNER"];
break;
}
// override if there is an inner EAP
if ($eap["INNER"] > 0) { // there is an inner EAP method
$out['EAP'] = 1;
}
return $out;
}
/**
*
* @param string $attrName the attribute name
* @return array of values for this attribute
*/
private function getSimpleMLAttribute($attrName)
{
if (empty($this->attributes[$attrName][0])) {
return([]);
}
$attributeList = $this->attributes[$attrName];
$objs = [];
if ($this->langScope === 'global') {
foreach ($attributeList['langs'] as $language => $value) {
$language = ($language === 'C' ? 'any' : $language);
$obj = new \core\DeviceXMLmain();
$obj->setValue($value);
$obj->setAttributes(['lang' => $language]);
$objs[] = $obj;
}
} else {
$objs[] = $attributeList[0];
}
return($objs);
}
/**
* constructs the name of the institution and puts it into the XML.
* consists of the best-language-match inst name, and if the inst has more
* than one profile also the best-language-match profile name
*
* @return \core\DeviceXMLmain[]
*/
private function getDisplayName()
{
$attr = $this->attributes;
$objs = [];
if ($this->langScope === 'global') {
$instNameLangs = $attr['general:instname']['langs'];
if ($attr['internal:profile_count'][0] > 1) {
$profileNameLangs = $attr['profile:name']['langs'];
}
foreach ($instNameLangs as $language => $value) {
$language = ($language === 'C' ? 'any' : $language);
$displayname = new \core\DeviceXMLmain();
if (isset($profileNameLangs)) {
$langOrC = isset($profileNameLangs[$language]) ? $profileNameLangs[$language] : $profileNameLangs['C'];
$value .= ' - '.$langOrC;
}
$displayname->setValue($value);
$displayname->setAttributes(['lang' => $language]);
$objs[] = $displayname;
}
} else {
$displayname = new \core\DeviceXMLmain();
$value = $attr['general:instname'][0];
if ($attr['internal:profile_count'][0] > 1) {
$value .= ' - '.$attr['profile:name'][0];
}
$displayname->setValue($value);
$objs[] = $displayname;
}
return $objs;
}
/**
* retrieves the provider logo and puts it into the XML structure
*
* @return \core\DeviceXMLmain
*/
private function getProviderLogo()
{
$attr = $this->attributes;
if (isset($attr['general:logo_file'][0])) {
$logoString = base64_encode($attr['general:logo_file'][0]);
$logoMime = 'image/'.$attr['internal:logo_file'][0]['mime'];
$providerlogo = new \core\DeviceXMLmain();
$providerlogo->setAttributes(['mime' => $logoMime, 'encoding' => 'base64']);
$providerlogo->setValue($logoString);
return $providerlogo;
}
return NULL;
}
/**
* retrieves provider information and puts it into the XML structure.
* contains the profile description and the ToU file, if any
*
* @return \core\DeviceXMLmain
*/
private function getProviderInfo()
{
$providerinfo = new \core\DeviceXMLmain();
$providerinfo->setChild('DisplayName', $this->getDisplayName());
$providerinfo->setChild('Description', $this->getSimpleMLAttribute('profile:description'));
$providerinfo->setChild('ProviderLocation', $this->getProviderLocation());
$providerinfo->setChild('ProviderLogo', $this->getProviderLogo());
$providerinfo->setChild('TermsOfUse', $this->getProviderTou(), null, 'cdata');
$providerinfo->setChild('Helpdesk', $this->getHelpdesk());
return $providerinfo;
}
private function getProviderTou() {
$standardTou = $this->getSimpleMLAttribute('support:info_file');
if ($this->addORtou === false) {
return $standardTou;
}
$out = [];
if ($this->langScope === 'global') {
foreach ($standardTou as $touObj) {
$tou = $touObj->getValue();
$lngAttr = $touObj->getAttributes();
$lng = $lngAttr['lang'] === 'any' ? \config\Master::APPEARANCE['defaultlocale'] : $lngAttr['lang'];
$tou .= "\n".$this->openRoamingToUArray[$lng];
$touObj->setValue($tou);
$out[] = $touObj;
}
} else {
$tou = $standardTou[0];
$tou .= "\n".$this->openRoamingToU;
$out = [$tou];
}
return $out;
}
/**
* retrieves the location information and puts it into the XML structure
*
* @return \core\DeviceXMLmain[]
*/
private function getProviderLocation()
{
$attr = $this->attributes;
if (isset($attr['general:geo_coordinates'])) {
$attrCoordinates = $attr['general:geo_coordinates'];
$location = [];
foreach ($attrCoordinates as $a) {
$providerlocation = new \core\DeviceXMLmain();
$b = json_decode($a, true);
$providerlocation->setChild('Longitude', $b['lon']);
$providerlocation->setChild('Latitude', $b['lat']);
$location[] = $providerlocation;
}
return $location;
}
return NULL;
}
/**
* retrieves helpdesk contact information and puts it into the XML structure
*
* @return \core\DeviceXMLmain
*/
private function getHelpdesk()
{
$helpdesk = new \core\DeviceXMLmain();
$helpdesk->setChild('EmailAddress', $this->getSimpleMLAttribute('support:email'));
$helpdesk->setChild('WebAddress', $this->getSimpleMLAttribute('support:url'));
$helpdesk->setChild('Phone', $this->getSimpleMLAttribute('support:phone'));
return $helpdesk;
}
/**
* determine where this credential should be applicable
*
* @return \core\DeviceXMLmain
*/
private function getCredentialApplicability($ssids, $oids)
{
$setWired = isset($this->attributes['media:wired'][0]) &&
$this->attributes['media:wired'][0] == 'on' ? 1 : 0;
$credentialapplicability = new \core\DeviceXMLmain();
$ieee80211s = [];
foreach ($ssids as $ssid) {
$ieee80211 = new \core\DeviceXMLmain();
$ieee80211->setChild('SSID', $ssid);
$ieee80211->setChild('MinRSNProto', 'CCMP');
$ieee80211s[] = $ieee80211;
}
foreach ($oids as $oid) {
$ieee80211 = new \core\DeviceXMLmain();
$ieee80211->setChild('ConsortiumOID', $oid);
$ieee80211s[] = $ieee80211;
}
$credentialapplicability->setChild('IEEE80211', $ieee80211s);
if ($setWired) {
$credentialapplicability->setChild('IEEE8023', '');
}
return $credentialapplicability;
}
/**
* retrieves the parameters needed for the given EAP method and creates
* appropriate nodes in the XML structure for them
*
* @param array $eap the EAP type in question
* @return array a recap of the findings
*/
private function getAuthenticationMethodParams($eap)
{
$inner = $this->innerAuth($eap);
$outerMethod = $eap["OUTER"];
if (isset($inner["METHOD"]) && $inner["METHOD"]) {
$innerauthmethod = new \core\DeviceXMLmain();
$typeOfInner = ($inner["EAP"] ? 'EAPMethod' : 'NonEAPAuthMethod');
$eapmethod = new \core\DeviceXMLmain();
$eapmethod->setChild('Type', abs($inner['METHOD']));
$innerauthmethod->setChild($typeOfInner, $eapmethod);
return ['inner_method' => $innerauthmethod, 'methodID' => $outerMethod, 'inner_methodID' => $inner['METHOD']];
} else {
return ['inner_method' => 0, 'methodID' => $outerMethod, 'inner_methodID' => 0];
}
}
/**
* sets the server-side credentials for a given EAP type
*
* @param \devices\XML\Type $eaptype the EAP type
* @return \core\DeviceXMLmain
*/
private function getServerSideCredentials($eap)
{
$attr = $this->attributes;
$children = $this->authMethodElements['server'][$eap];
$serversidecredential = new \core\DeviceXMLmain();
// Certificates and server names
$cAlist = [];
$attrCaList = $attr['internal:CAs'][0];
foreach ($attrCaList as $ca) {
$caObject = new \core\DeviceXMLmain();
$caObject->setValue(base64_encode($ca['der']));
$caObject->setAttributes(['format' => 'X.509', 'encoding' => 'base64']);
$cAlist[] = $caObject;
}
$serverids = [];
$servers = $attr['eap:server_name'];
foreach ($servers as $server) {
$serverid = new \core\DeviceXMLmain();
$serverid->setValue($server);
$serverids[] = $serverid;
}
if (in_array('CA', $children)) {
$serversidecredential->setChild('CA', $cAlist);
}
if (in_array('ServerID', $children)) {
$serversidecredential->setChild('ServerID', $serverids);
}
return $serversidecredential;
}
/**
* sets the realm information for the client-side credential
*
* @param \core\DeviceXMLmain $clientsidecredential the ClientSideCredential to which the realm info is to be added
* @return void
*/
private function setClientSideRealm($clientsidecredential)
{
$attr = $this->attributes;
$realm = \core\common\Entity::getAttributeValue($attr, 'internal:realm', 0);
if ($realm === NULL) {
return;
}
if (\core\common\Entity::getAttributeValue($attr, 'internal:verify_userinput_suffix', 0) !== 1) {
return;
}
$clientsidecredential->setChild('InnerIdentitySuffix', $realm);
if (\core\common\Entity::getAttributeValue($attr, 'internal:hint_userinput_suffix', 0) === 1) {
$clientsidecredential->setChild('InnerIdentityHint', 'true');
}
}
/**
* sets the client certificate
*
* @return \core\DeviceXMLmain
*/
private function getClientCertificate()
{
$clientCertificateObject = new \core\DeviceXMLmain();
$clientCertificateObject->setValue(base64_encode($this->clientCert["certdata"]));
$clientCertificateObject->setAttributes(['format' => 'PKCS12', 'encoding' => 'base64']);
return $clientCertificateObject;
}
/**
* sets the client-side credentials for the given EAP type
*
* @param array $eapParams the EAP parameters
* @return \core\DeviceXMLmain
*/
private function getClientSideCredentials($eap)
{
$children = $this->authMethodElements['client'][$eap];
$clientsidecredential = new \core\DeviceXMLmain();
$outerId = $this->determineOuterIdString();
$this->loggerInstance->debug(5, $eap, "XMLOI:", "\n");
if (in_array('OuterIdentity', $children)) {
if ($outerId !== NULL) {
$clientsidecredential->setChild('OuterIdentity', $outerId);
}
}
$this->setClientSideRealm($clientsidecredential);
// $clientsidecredential->setChild('EAPType', $eapParams['inner_methodID'] ? $eapParams['inner_methodID'] : $eapParams['methodID']);
// Client Certificate
if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
$attr = $this->attributes;
$outerId = \core\common\Entity::getAttributeValue($attr, 'internal:username', 0);
$clientsidecredential->setChild('OuterIdentity', $outerId);
$clientsidecredential->setChild('ClientCertificate', $this->getClientCertificate());
}
return $clientsidecredential;
}
/**
* sets the EAP method
*
* @param \devices\XML\Type $eaptype the EAP type XMLObject
* @return \core\DeviceXMLmain
*/
private function getEapMethod($eaptype)
{
$eapmethod = new \core\DeviceXMLmain();
$eapmethod->setChild('Type', $eaptype);
if (isset($this->VendorSpecific)) {
$vendorspecifics = [];
foreach ($this->VendorSpecific as $vs) {
$vendorspecific = new \core\DeviceXMLmain();
$vs['value']->addAttribute('xsi:noNamespaceSchemaLocation', "xxx.xsd");
$vendorspecific->setValue($vs['value']);
$vendorspecific->setAttributes(['vendor' => $vs['vendor']]);
$vendorspecifics[] = $vendorspecific;
}
$eapmethod->setChild('VendorSpecific', $vendorspecifics);
}
return($eapmethod);
}
/**
* determines the authentication method to use
*
* @param array $eap the EAP methods, in array representation
* @return \core\DeviceXMLmain
*/
private function getAuthMethod($eap)
{
$authmethod = new \core\DeviceXMLmain();
$eapParams = $this->getAuthenticationMethodParams($eap);
$eaptype = new \core\DeviceXMLmain();
$eaptype->setValue($eapParams['methodID']);
// Type
$authmethod->setChild('EAPMethod', $this->getEapMethod($eaptype));
// ServerSideCredentials
$authmethod->setChild('ServerSideCredential', $this->getServerSideCredentials($eap['OUTER']));
// ClientSideCredentials
$authmethod->setChild('ClientSideCredential', $this->getClientSideCredentials($eap['INNER']));
if ($eapParams['inner_method']) {
$authmethod->setChild('InnerAuthenticationMethod', $eapParams['inner_method']);
}
return $authmethod;
}
private function getAuthMethodsList() {
$methodList = [];
if ($this->allEaps) {
$eapmethods = [];
foreach ($this->attributes['all_eaps'] as $eap) {
$eapRep = $eap->getArrayRep();
if (in_array($eapRep, $this->supportedEapMethods)) {
$eapmethods[] = $eapRep;
}
}
} else {
$eapmethods = [$this->selectedEap];
}
foreach ($eapmethods as $eap) {
$methodList[] = $this->getAuthMethod($eap);
}
return $methodList;
}
private $openRoamingToUArray;
private $openRoamingToU;
private $addORtou = false;
}