Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: samlp elements #4

Merged
merged 19 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
- name: Lint Code Base
uses: super-linter/super-linter/slim@v7
env:
SAVE_SUPER_LINTER_OUTPUT: false
# To report GitHub Actions status checks
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LINTER_RULES_PATH: 'tools/linters'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
/composer.lock
/composer.phar
phpunit.xml.bak
/super-linter-output/
/vendor/
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@

"psr/clock": "^1.0",
"psr/log": "^2.0 | ^3.0",
"simplesamlphp/assert": "^1.0",
"simplesamlphp/xml-common": "^1.14",
"simplesamlphp/xml-security": "^1.6"
"simplesamlphp/assert": "^1.6",
"simplesamlphp/xml-common": "~1.21.0",
"simplesamlphp/xml-security": "~1.11.0"
},
"require-dev": {
"beste/clock": "^3.0",
"simplesamlphp/simplesamlphp-test-framework": "^1.7"
"simplesamlphp/simplesamlphp-test-framework": "^1.8"
},
"autoload": {
"psr-4": {
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
<!-- Lines can be a little bit longer before they break the build -->
<rule ref="Generic.Files.LineLength">
<exclude-pattern>tests/src/SAML11/XML/saml/AttributeValueTest.php</exclude-pattern>
<exclude-pattern>tests/src/SAML11/XML/samlp/StatusDetailTest.php</exclude-pattern>
</rule>
</ruleset>
30 changes: 30 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,33 @@ parameters:
message: "#^Access to undefined constant static\\(SimpleSAML\\\\SAML11\\\\XML\\\\saml\\\\AbstractSubjectStatement\\)\\:\\:XSI_TYPE_PREFIX\\.$#"
count: 2
path: src/SAML11/XML/saml/AbstractSubjectStatement.php

-
message: "#^Access to undefined constant static\\(SimpleSAML\\\\SAML11\\\\XML\\\\samlp\\\\AbstractQuery\\)\\:\\:XSI_TYPE_NAME\\.$#"
count: 2
path: src/SAML11/XML/samlp/AbstractQuery.php

-
message: "#^Access to undefined constant static\\(SimpleSAML\\\\SAML11\\\\XML\\\\samlp\\\\AbstractQuery\\)\\:\\:XSI_TYPE_NAMESPACE\\.$#"
count: 2
path: src/SAML11/XML/samlp/AbstractQuery.php

-
message: "#^Access to undefined constant static\\(SimpleSAML\\\\SAML11\\\\XML\\\\samlp\\\\AbstractQuery\\)\\:\\:XSI_TYPE_PREFIX\\.$#"
count: 2
path: src/SAML11/XML/samlp/AbstractQuery.php

-
message: "#^Access to undefined constant static\\(SimpleSAML\\\\SAML11\\\\XML\\\\samlp\\\\AbstractSubjectQuery\\)\\:\\:XSI_TYPE_NAME\\.$#"
count: 2
path: src/SAML11/XML/samlp/AbstractSubjectQuery.php

-
message: "#^Access to undefined constant static\\(SimpleSAML\\\\SAML11\\\\XML\\\\samlp\\\\AbstractSubjectQuery\\)\\:\\:XSI_TYPE_NAMESPACE\\.$#"
count: 2
path: src/SAML11/XML/samlp/AbstractSubjectQuery.php

-
message: "#^Access to undefined constant static\\(SimpleSAML\\\\SAML11\\\\XML\\\\samlp\\\\AbstractSubjectQuery\\)\\:\\:XSI_TYPE_PREFIX\\.$#"
count: 2
path: src/SAML11/XML/samlp/AbstractSubjectQuery.php
2 changes: 2 additions & 0 deletions src/SAML11/Assert/CustomAssertionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ private static function validURI(string $value, string $message = ''): void

try {
BaseAssert::notWhitespaceOnly($value, $message ?: '%s is not a SAML1.1-compliant URI');
// If it doesn't have a scheme, it's not an absolute URI
BaseAssert::regex($value, self::$scheme_regex, $message ?: '%s is not a SAML1.1-compliant URI');
} catch (AssertionFailedException $e) {
throw new ProtocolViolationException($e->getMessage());
}
Expand Down
9 changes: 8 additions & 1 deletion src/SAML11/Compat/AbstractContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use SimpleSAML\SAML11\XML\ExtensionPointInterface;
use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XML\Exception\SchemaViolationException;
use SimpleSAML\XMLSecurity\Alg\Encryption\EncryptionAlgorithmFactory;
use SimpleSAML\XMLSecurity\Alg\KeyTransport\KeyTransportAlgorithmFactory;
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;

use function array_key_exists;
use function implode;
Expand All @@ -24,7 +27,11 @@ abstract class AbstractContainer
protected array $registry = [];

/** @var array|null */
protected ?array $blacklistedEncryptionAlgorithms;
protected ?array $blacklistedEncryptionAlgorithms = [
EncryptionAlgorithmFactory::DEFAULT_BLACKLIST,
KeyTransportAlgorithmFactory::DEFAULT_BLACKLIST,
SignatureAlgorithmFactory::DEFAULT_BLACKLIST,
];


/**
Expand Down
77 changes: 76 additions & 1 deletion src/SAML11/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ class Constants extends \SimpleSAML\XMLSecurity\Constants
*/
public const NAMEID_UNSPECIFIED = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified';


/**
* Windows Domain Qualifier Name NameID format.
*/
Expand All @@ -102,4 +101,80 @@ class Constants extends \SimpleSAML\XMLSecurity\Constants
* The namespace for the SAML 1.1 protocol.
*/
public const NS_SAMLP = 'urn:oasis:names:tc:SAML:1.0:protocol';

/**
* The SAML responder or SAML authority is able to process the request but has chosen not to respond.
* This status code MAY be used when there is concern about the security context of the request message or
* the sequence of request messages received from a particular requester.
*
* Second-level status code.
*/
public const STATUS_REQUEST_DENIED = 'samlp:RequestDenied';

/**
* The SAML responder cannot process any requests with the protocol version specified in the request.
*
* Second-level status code.
*/
public const STATUS_REQUEST_VERSION_DEPRECATED = 'samlp:RequestVersionDeprecated';

/**
* The SAML responder cannot process the request because the protocol version specified in the request message
* is a major upgrade from the highest protocol version supported by the responder.
*
* Second-level status code.
*/
public const STATUS_REQUEST_VERSION_TOO_HIGH = 'samlp:RequestVersionTooHigh';

/**
* The SAML responder cannot process the request because the protocol version specified in the request message
* is too low.
*
* Second-level status code.
*/
public const STATUS_REQUEST_VERSION_TOO_LOW = 'samlp:RequestVersionTooLow';

/**
* The request could not be performed due to an error on the part of the requester.
*
* Top-level status code.
*/
public const STATUS_REQUESTER = 'samlp:Requester';

/**
* The resource value provided in the request message is invalid or unrecognized.
*
* Second-level status code.
*/
public const STATUS_RESOURCE_NOT_RECOGNIZED = 'samlp:ResourceNotRecognized';

/**
* The request could not be performed due to an error on the part of the SAML responder or SAML authority.
*
* Top-level status code.
*/
public const STATUS_RESPONDER = 'samlp:Responder';

/**
* Top-level status code indicating successful processing of the request.
* The request succeeded. Additional information MAY be returned in the <StatusMessage>
* and/or <StatusDetail> elements.
*
* Top-level status code.
*/
public const STATUS_SUCCESS = 'samlp:Success';

/**
* The response message would contain more elements than the SAML responder is able to return.
*
* Second-level status code.
*/
public const STATUS_TOO_MANY_RESPONSES = 'samlp:TooManyResponses';

/**
* The SAML responder could not process the request because the version of the request message was incorrect.
*
* Top-level status code.
*/
public const STATUS_VERSION_MISMATCH = 'samlp:VersionMismatch';
}
4 changes: 2 additions & 2 deletions src/SAML11/Exception/ProtocolViolationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class ProtocolViolationException extends RuntimeException
/**
* @param string $message
*/
public function __construct(string $message = null)
public function __construct(string $message = '')
{
if ($message === null) {
if ($message === '') {
if (defined('static::DEFAULT_MESSAGE')) {
$message = static::DEFAULT_MESSAGE;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
namespace SimpleSAML\SAML11\Exception;

/**
* This exception may be raised when a violation of the SAML11 specification is detected
* This exception may be raised when a message with a wrong version is received.
*
* @package simplesamlphp/saml11
*/
class ProtocolViolationException extends RuntimeException
class VersionMismatchException extends RuntimeException
{
/**
* @param string $message
*/
public function __construct(string $message = null)
public function __construct(string $message = '')
{
if ($message === null) {
if ($message === '') {
if (defined('static::DEFAULT_MESSAGE')) {
$message = static::DEFAULT_MESSAGE;
} else {
$message = 'A violation of the SAML11 protocol occurred.';
$message = 'A message with the wrong version was received.';
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/SAML11/XML/ExtensionPointTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
*/
trait ExtensionPointTrait
{
/**
* @inheritDoc
*/
public function getXsiType(): string
{
return $this->type;
}


/**
* Get the local name for the element's xsi:type.
*
Expand Down
133 changes: 133 additions & 0 deletions src/SAML11/XML/SignableElementTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML11\XML;

use DOMElement;
use SimpleSAML\Assert\Assert;
use SimpleSAML\SAML11\Compat\ContainerSingleton;
use SimpleSAML\XML\DOMDocumentFactory;
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface;
use SimpleSAML\XMLSecurity\Constants as C;
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
use SimpleSAML\XMLSecurity\Exception\UnsupportedAlgorithmException;
use SimpleSAML\XMLSecurity\Utils\XML;
use SimpleSAML\XMLSecurity\XML\ds\CanonicalizationMethod;
use SimpleSAML\XMLSecurity\XML\ds\KeyInfo;
use SimpleSAML\XMLSecurity\XML\ds\Signature;
use SimpleSAML\XMLSecurity\XML\ds\SignatureMethod;
use SimpleSAML\XMLSecurity\XML\ds\SignatureValue;
use SimpleSAML\XMLSecurity\XML\ds\SignedInfo;
use SimpleSAML\XMLSecurity\XML\ds\Transform;
use SimpleSAML\XMLSecurity\XML\ds\Transforms;
use SimpleSAML\XMLSecurity\XML\SignableElementTrait as BaseSignableElementTrait;

use function base64_encode;

/**
* Helper trait for processing signable elements.
*
* @package simplesamlphp/saml11
*/
trait SignableElementTrait
{
use BaseSignableElementTrait;


/**
* Sign the current element.
*
* The signature will not be applied until toXML() is called.
*
* @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface $signer The actual signer implementation
* to use.
* @param string $canonicalizationAlg The identifier of the canonicalization algorithm to use.
* @param \SimpleSAML\XMLSecurity\XML\ds\KeyInfo|null $keyInfo A KeyInfo object to add to the signature.
*/
public function sign(
SignatureAlgorithmInterface $signer,
string $canonicalizationAlg = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS,
?KeyInfo $keyInfo = null,
): void {
/**
* 5.4.2: SAML assertions and protocol messages MUST supply a value for the ID attribute
* on the root element of the assertion or protocol message being signed.
*/
Assert::notNull($this->getID(), "Signable element must have an ID set before it can be signed.");

$this->signer = $signer;
$this->keyInfo = $keyInfo;
Assert::oneOf(
$canonicalizationAlg,
[
C::C14N_INCLUSIVE_WITH_COMMENTS,
C::C14N_INCLUSIVE_WITHOUT_COMMENTS,
C::C14N_EXCLUSIVE_WITH_COMMENTS,
C::C14N_EXCLUSIVE_WITHOUT_COMMENTS,
],
'Unsupported canonicalization algorithm: %s',
UnsupportedAlgorithmException::class,
);
$this->c14nAlg = $canonicalizationAlg;
}


/**
* Do the actual signing of the document.
*
* Note that this method does not insert the signature in the returned \DOMElement. The signature will be available
* in $this->signature as a \SimpleSAML\XMLSecurity\XML\ds\Signature object, which can then be converted to XML
* calling toXML() on it, passing the \DOMElement value returned here as a parameter. The resulting \DOMElement
* can then be inserted in the position desired.
*
* E.g.:
* $xml = // our XML to sign
* $signedXML = $this->doSign($xml);
* $signedXML->appendChild($this->signature->toXML($signedXML));
*
* @param \DOMElement $xml The element to sign.
* @return \DOMElement The signed element, without the signature attached to it just yet.
*/
protected function doSign(DOMElement $xml): DOMElement
{
Assert::notNull(
$this->signer,
'Cannot call toSignedXML() without calling sign() first.',
RuntimeException::class,
);

$algorithm = $this->signer->getAlgorithmId();
$digest = $this->signer->getDigest();

$transforms = new Transforms([
/**
* 5.4.1: SAML assertions and protocols MUST use enveloped signatures when
* signing assertions and protocol messages
*/
new Transform(C::XMLDSIG_ENVELOPED),
new Transform($this->c14nAlg),
]);

$canonicalDocument = XML::processTransforms($transforms, $xml);

$signedInfo = new SignedInfo(
new CanonicalizationMethod($this->c14nAlg),
new SignatureMethod($algorithm),
[$this->getReference($digest, $transforms, $xml, $canonicalDocument)],
);

$signingData = $signedInfo->canonicalize($this->c14nAlg);
$signedData = base64_encode($this->signer->sign($signingData));

$this->signature = new Signature($signedInfo, new SignatureValue($signedData), $this->keyInfo);
return DOMDocumentFactory::fromString($canonicalDocument)->documentElement;
}


public function getBlacklistedAlgorithms(): ?array
{
$container = ContainerSingleton::getInstance();
return $container->getBlacklistedEncryptionAlgorithms();
}
}
Loading
Loading