Skip to content

Commit

Permalink
Merge pull request #66 from italia/feature/issue-65-certs
Browse files Browse the repository at this point in the history
fix #65
  • Loading branch information
Lorenzo Cattaneo authored Oct 26, 2018
2 parents 0d5d54c + 2cec637 commit 178a7fc
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 20 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ cache:
install:
- composer install --no-interaction
- mkdir -p example/idp_metadata
- make -C example
- php bin/download_idp_metadata.php ./example/idp_metadata

script:
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ $settings = array(
],
'sp_org_name' => 'your organization full name',
'sp_org_display_name' => 'your organization display name',
'sp_key_cert_values' => [
'countryName' => 'Your Country',
'stateOrProvinceName' => 'Your Province or State',
'localityName' => 'Locality',
'commonName' => 'Name',
'emailAddress' => '[email protected]',
]
'idp_metadata_folder' => '/path/to/idp_metadata/',
'sp_attributeconsumingservice' => [
// order is important ! the 0-base index in this array will be used as ID in the calls
Expand Down Expand Up @@ -196,10 +203,12 @@ The method will redirect to the IdP Single Logout page, or return false if you a
|getSPMetadata() : string|returns the SP metadata as a string|
|login(string $idpFilename, int $assertID, int $attrID, $level = 1, string $redirectTo = null, $shouldRedirect = true)|login with REDIRECT binding. Use `$idpFilename` to select in IdP for login by indicating the name (without extension) of an XML file in your `idp_metadata_folder`. `$assertID` and `$attrID` indicate respectively the array index of `sp_assertionconsumerservice` and `sp_attributeconsumingservice` provided in settings. Optional parameters: `$level` for SPID authentication level (1, 2 or 3), `$redirectTo` to indicate an url to redirect to after login, `$shouldRedirect` to indicate if the login function should automatically redirect to the IdP or should return the login url as a string|
|loginPost(string $idpName, int $ass, int $attr, $level = 1, string $redirectTo = null, $shouldRedirect = true)|like login, but uses POST binding|
|public function logout(int $slo, string $redirectTo = null, $shouldRedirect = true)|logout with REDIRECT binding. `$slo` indicates the array index of the `sp_singlelogoutservice` provided in settings. Optional parameters: `$redirectTo` to indicate an url to redirect to after login, `$shouldRedirect` to indicate if the login function should automatically redirect to the IdP or should return the login url as a string|
|logout(int $slo, string $redirectTo = null, $shouldRedirect = true)|logout with REDIRECT binding. `$slo` indicates the array index of the `sp_singlelogoutservice` provided in settings. Optional parameters: `$redirectTo` to indicate an url to redirect to after login, `$shouldRedirect` to indicate if the login function should automatically redirect to the IdP or should return the login url as a string|
|logoutPost(int $slo, string $redirectTo = null, $shouldRedirect = true)|like logout, but uses POST binding|
|isAuthenticated() : bool|checks if the user is authenticated. This method **MUST** be caled after login and logout to finalise the operation.|
|getAttributes() : array|If you requested attributes with an attribute consuming service during login, this method will return them in array format|
|isConfigured() : bool|Returns true if the SP certificates are found where the settings says they are, and they are valid (i.e. the library has been configured correctly)|
|configure(string $countryName, string $stateName, string $localityName, string $commonName, string $emailAddress)|Generates the SP key and certificate (validity = 10 years) where the settings says they should be; this function should be used with care because it requires write access to the filessystem, and invalidates the metadata|

### Example

Expand Down Expand Up @@ -339,7 +348,6 @@ cd vendor/italia/spid-php-lib
Install prerequisites with composer, generate key and certificate for the SP and download the metadata for all current production IdPs with:
```sh
composer install
make -C example/
bin/download_idp_metadata.php example/idp_metadata
```
Expand Down
7 changes: 7 additions & 0 deletions example/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
],
'sp_org_name' => 'test',
'sp_org_display_name' => 'Test',
'sp_key_cert_values' => [
'countryName' => 'IT',
'stateOrProvinceName' => 'Milan',
'localityName' => 'Milan',
'commonName' => 'Name',
'emailAddress' => '[email protected]',
],
'idp_metadata_folder' => './idp_metadata/',
'sp_attributeconsumingservice' => [
["name", "familyName", "fiscalNumber", "email"],
Expand Down
8 changes: 4 additions & 4 deletions src/Sp.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@ class Sp
*/
private $protocol;

public function __construct(array $settings, String $protocol = null)
public function __construct(array $settings, String $protocol = null, $autoconfigure = true)
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
switch ($protocol) {
case 'saml':
$this->protocol = new Spid\Saml($settings);
$this->protocol = new Spid\Saml($settings, $autoconfigure);
break;
default:
$this->protocol = new Spid\Saml($settings);
$this->protocol = new Spid\Saml($settings, $autoconfigure);
}
}

public function __call($method, $arguments)
{
$methods_implemented = get_class_methods($this->protocol);
if (!in_array($method, $methods_implemented)) {
throw new \Exception("Invalid method requested", 1);
throw new \Exception("Invalid method [$method] requested", 1);
}
return call_user_func_array(array($this->protocol, $method), $arguments);
}
Expand Down
12 changes: 11 additions & 1 deletion src/Spid/Interfaces/SAMLInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,24 @@ interface SAMLInterface
// ],
// 'sp_org_name' => 'your organization full name',
// 'sp_org_display_name' => 'your organization display name',
// 'sp_key_cert_values' => [
// 'countryName' => 'CN',
// 'stateOrProvinceName' => 'State',
// 'localityName' => 'Locality',
// 'commonName' => 'Name',
// 'emailAddress' => '[email protected]',
// ],
// 'idp_metadata_folder' => '/path/to/idp_metadata/',
// 'sp_attributeconsumingservice' => [
// // order is important ! the 0-base index in this array will be used as ID in the calls
// ["fiscalNumber"],
// ["name", "familyName", "fiscalNumber", "email", "spidCode"],
// ...
// ];
public function __construct(array $settings);
//
// $autoconfigure: boolean value, determines if SP key and cert files should be autogenerated base on values provided in settings
// If set to false this step will be skipped
public function __construct(array $settings, $autoconfigure = true);

// loads an Idp object by parsing the provided XML at $filename
// $filename: file name of the IdP to be loaded. Only the file, without the path, needs to be provided.
Expand Down
48 changes: 46 additions & 2 deletions src/Spid/Saml.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
use Italia\Spid\Spid\Saml\Settings;
use Italia\Spid\Spid\Saml\SignatureUtils;
use Italia\Spid\Spid\Interfaces\SAMLInterface;
use Italia\Spid\Spid\Session;

class Saml implements SAMLInterface
{
public $settings;
private $idps = []; // contains filename -> Idp object array
private $session; // Session object

public function __construct(array $settings)
public function __construct(array $settings, $autoconfigure = true)
{
Settings::validateSettings($settings);
$this->settings = $settings;

// Do not attemp autoconfiguration if key and cert values have not been set
if (!array_key_exists('sp_key_cert_values', $this->settings)) {
$autoconfigure = false;
}
if ($autoconfigure && !$this->isConfigured()) {
$this->configure();
}
}

public function loadIdpFromFile(string $filename)
Expand Down Expand Up @@ -229,4 +238,39 @@ public function getAttributes() : array
}
return $this->session->attributes;
}
}

// returns true if the SP certificates are found where the settings says they are, and they are valid
// (i.e. the library has been configured correctly
private function isConfigured() : bool
{
if (!is_readable($this->settings['sp_key_file'])) {
return false;
}
if (!is_readable($this->settings['sp_cert_file'])) {
return false;
}
$key = file_get_contents($this->settings['sp_key_file']);
if (!openssl_get_privatekey($key)) {
return false;
}
$cert = file_get_contents($this->settings['sp_cert_file']);
if (!openssl_get_publickey($cert)) {
return false;
}
if (!SignatureUtils::certDNEquals($cert, $this->settings)) {
return false;
}
return true;
}

// Generates with openssl the SP certificates where the settings says they should be
// this function should be used with care because it requires write access to the filesystem, and invalidates the metadata
private function configure()
{
$keyCert = SignatureUtils::generateKeyCert($this->settings);
file_put_contents($this->settings['sp_key_file'], $keyCert['key']);
file_put_contents($this->settings['sp_cert_file'], $keyCert['cert']);
}


}
72 changes: 62 additions & 10 deletions src/Spid/Saml/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@ class Settings
const BINDING_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect';
const BINDING_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST';

const REQUIRED = 1;
const NOT_REQUIRED = 0;
// Settings with value 1 are mandatory
private static $validSettings = [
'sp_entityid' => 1,
'sp_key_file' => 1,
'sp_cert_file' => 1,
'sp_assertionconsumerservice' => 1,
'sp_singlelogoutservice' => 1,
'sp_attributeconsumingservice' => 0,
'sp_org_name' => 0,
'sp_org_display_name' => 0,
'idp_metadata_folder' => 1
'sp_entityid' => self::REQUIRED,
'sp_key_file' => self::REQUIRED,
'sp_cert_file' => self::REQUIRED,
'sp_assertionconsumerservice' => self::REQUIRED,
'sp_singlelogoutservice' => self::REQUIRED,
'sp_attributeconsumingservice' => self::NOT_REQUIRED,
'sp_org_name' => self::NOT_REQUIRED,
'sp_org_display_name' => self::NOT_REQUIRED,
'sp_key_cert_values' => [
self::NOT_REQUIRED => [
'countryName' => self::REQUIRED,
'stateOrProvinceName' => self::REQUIRED,
'localityName' => self::REQUIRED,
'commonName' => self::REQUIRED,
'emailAddress' => self::REQUIRED
]
],
'idp_metadata_folder' => self::REQUIRED
];

private static $validAttributeFields = [
Expand Down Expand Up @@ -45,8 +56,23 @@ public static function validateSettings(array $settings)
$missingSettings = array();
$msg = 'Missing settings fields: ';
array_walk(self::$validSettings, function ($v, $k) use (&$missingSettings, &$settings) {
if (self::$validSettings[$k] == 1 && !array_key_exists($k, $settings)) {
$settingRequired = self::$validSettings[$k];
$childSettings = [];
if (is_array($v) && isset($v[self::REQUIRED])) {
$settingRequired = self::REQUIRED;
$childSettings[$k] = $v[self::REQUIRED];
}
if ($settingRequired == self::REQUIRED && !array_key_exists($k, $settings)) {
$missingSettings[$k] = 1;
} else {
foreach ($childSettings as $key => $value) {
if (
$value == self::REQUIRED &&
!array_key_exists($key, $settings[$k])
) {
$missingSettings[$key] = 1;
}
}
}
});
foreach ($missingSettings as $k => $v) {
Expand All @@ -57,6 +83,16 @@ public static function validateSettings(array $settings)
}

$invalidFields = array_diff_key($settings, self::$validSettings);
// Check for settings that have child values
array_walk(self::$validSettings, function($v, $k) use (&$invalidFields) {
// Child values found, check if settings array is set for that key
if (is_array($v) && isset($settings[$k])) {
// $v has at most 2 keys, self::REQUIRED and self::NOT_REQUIRED
// do array_dif_key for both sub arrays
$invalidFields = array_merge($invalidFields, array_diff_key($settings[$k], reset($v)));
$invalidFields = array_merge($invalidFields, array_diff_key($settings[$k], end($v)));
}
});
$msg = 'Invalid settings fields: ';
foreach ($invalidFields as $k => $v) {
$msg .= $k . ', ';
Expand Down Expand Up @@ -147,5 +183,21 @@ private static function checkSettingsValues($settings)
', got ' . parse_url($slo[0], PHP_URL_HOST) . 'instead');
}
});
if (isset($settings['sp_key_cert_values'])) {
if (!is_array($settings['sp_key_cert_values'])) {
throw new \Exception('sp_key_cert_values should be an array');
}
if (count($settings['sp_key_cert_values']) != 5) {
throw new \Exception('sp_key_cert_values should contain 5 values: countryName, stateOrProvinceName, localityName, commonName, emailAddress');
}
foreach ($settings['sp_key_cert_values'] as $key => $value) {
if (!is_string($value)) {
throw new \Exception('sp_key_cert_values values should be strings. Valued provided for key ' . $key . ' is not a string');
}
}
if (strlen($settings['sp_key_cert_values']['countryName']) != 2) {
throw new \Exception('sp_key_cert_values countryName should be a 2 characters country code');
}
}
}
}
46 changes: 46 additions & 0 deletions src/Spid/Saml/SignatureUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,52 @@ public static function validateXmlSignature($xml, $cert) : bool
return false;
}

public static function certDNEquals($cert, $settings)
{
$parsed = openssl_x509_parse($cert);
$dn = $parsed['subject'];

$newDN = array();
$newDN[] = $settings['sp_org_name'] ?? [];
$newDN[] = $settings['sp_org_display_name'] ?? [];
$newDN = array_merge($newDN, $settings['sp_key_cert_values'] ?? []);
asort($dn);
asort($newDN);

if (array_values($dn) == array_values($newDN)) {
return true;
}
return false;
}

public static function generateKeyCert($settings) : array
{
$numberofdays = 3652 * 2;
$privkey = openssl_pkey_new(array(
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
));
$dn = array(
"countryName" => $settings['sp_key_cert_values']['countryName'],
"stateOrProvinceName" => $settings['sp_key_cert_values']['stateOrProvinceName'],
"localityName" => $settings['sp_key_cert_values']['localityName'],
"organizationName" => $orgName = $settings['sp_org_name'],
"organizationalUnitName" => $settings['sp_org_display_name'],
"commonName" => $settings['sp_key_cert_values']['commonName'],
"emailAddress" => $settings['sp_key_cert_values']['emailAddress']
);
$csr = openssl_csr_new($dn, $privkey, array('digest_alg' => 'sha256'));
$myserial = (int) hexdec(bin2hex(openssl_random_pseudo_bytes(8)));
$configArgs = array("digest_alg" => "sha256");
$sscert = openssl_csr_sign($csr, null, $privkey, $numberofdays, $configArgs, $myserial);
openssl_x509_export($sscert, $publickey);
openssl_pkey_export($privkey, $privatekey);
return [
'key' => $privatekey,
'cert' => $publickey
];
}

private static function query(\DOMDocument $dom, $query, \DOMElement $context = null)
{
$xpath = new \DOMXPath($dom);
Expand Down
32 changes: 32 additions & 0 deletions tests/SpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ final class SpTest extends PHPUnit\Framework\TestCase
],
'sp_org_name' => 'test_simevo',
'sp_org_display_name' => 'Test Simevo',
'sp_key_cert_values' => [
'countryName' => 'IT',
'stateOrProvinceName' => 'Milan',
'localityName' => 'Milan',
'commonName' => 'Name',
'emailAddress' => '[email protected]',
],
'idp_metadata_folder' => './example/idp_metadata/',
'sp_attributeconsumingservice' => [
["name", "familyName", "fiscalNumber", "email"],
Expand All @@ -29,6 +36,31 @@ public function testCanBeCreatedFromValidSettings()
Italia\Spid\Sp::class,
new Italia\Spid\Sp(SpTest::$settings)
);
$this->assertTrue(is_readable(self::$settings['sp_key_file']));
$this->assertTrue(is_readable(self::$settings['sp_cert_file']));
}

public function testCanBeCreatedWithoutAutoconfigure()
{
$settings = SpTest::$settings;
$settings['sp_key_file'] = './wrong/location/sp.key';
$settings['sp_cert_file'] = './wrong/location/sp.crt';
$this->assertInstanceOf(
Italia\Spid\Sp::class,
new Italia\Spid\Sp(SpTest::$settings, null, false)
);
$this->assertFalse(is_readable($settings['sp_key_file']));
$this->assertFalse(is_readable($settings['sp_cert_file']));
}

public function testCannotCreateNoKeyCert()
{
$this->assertInstanceOf(
Italia\Spid\Sp::class,
new Italia\Spid\Sp(SpTest::$settings, null, false)
);
$this->assertTrue(is_readable(self::$settings['sp_key_file']));
$this->assertTrue(is_readable(self::$settings['sp_cert_file']));
}

private function validateXml($xmlString, $schemaFile, $valid = true)
Expand Down

0 comments on commit 178a7fc

Please sign in to comment.