Skip to content

Commit

Permalink
Add custom SignedElementTrait to enforce SAML-specific rules for sign…
Browse files Browse the repository at this point in the history
…atures
  • Loading branch information
tvdijen committed Jul 18, 2024
1 parent 79c6b30 commit 7f32ac3
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 3 deletions.
333 changes: 333 additions & 0 deletions src/SAML2/XML/SignedElementTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2\XML;

use DOMElement;
use SimpleSAML\Assert\Assert;
use SimpleSAML\XML\DOMDocumentFactory;
use SimpleSAML\XML\Exception\TooManyElementsException;
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface;
use SimpleSAML\XMLSecurity\Constants as C;
use SimpleSAML\XMLSecurity\CryptoEncoding\PEM;
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
use SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException;
use SimpleSAML\XMLSecurity\Exception\ReferenceValidationFailedException;
use SimpleSAML\XMLSecurity\Exception\RuntimeException;
use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException;
use SimpleSAML\XMLSecurity\Key;
use SimpleSAML\XMLSecurity\Key\KeyInterface;
use SimpleSAML\XMLSecurity\Utils\XML;
use SimpleSAML\XMLSecurity\Utils\XPath;
use SimpleSAML\XMLSecurity\XML\ds\Reference;
use SimpleSAML\XMLSecurity\XML\ds\Signature;
use SimpleSAML\XMLSecurity\XML\ds\SignedInfo;
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
use SimpleSAML\XMLSecurity\XML\ds\X509Data;

use function array_pop;
use function base64_decode;
use function hash;
use function hash_equals;
use function in_array;

/**
* Helper trait for processing signed elements.
*
* @package simplesamlphp/xml-security
*/
trait SignedElementTrait

Check failure on line 41 in src/SAML2/XML/SignedElementTrait.php

View workflow job for this annotation

GitHub Actions / Interoperability tests, PHP 8.2, ubuntu-latest

Trait "SimpleSAML\SAML2\XML\CanonicalizableElementTrait" not found
{
use CanonicalizableElementTrait;

/**
* The signature of this element.
*
* @var \SimpleSAML\XMLSecurity\XML\ds\Signature|null $signature
*/
protected ?Signature $signature = null;

/**
* The key that successfully verifies the signature in this object.
*
* @var \SimpleSAML\XMLSecurity\Key\KeyInterface|null
*/
private ?KeyInterface $validatingKey = null;


/**
* Get the signature element of this object.
*
* @return \SimpleSAML\XMLSecurity\XML\ds\Signature
*/
public function getSignature(): ?Signature
{
return $this->signature;
}


/**
* Initialize a signed element from XML.
*
* @param \SimpleSAML\XMLSecurity\XML\ds\Signature $signature The ds:Signature object
*/
protected function setSignature(Signature $signature): void
{
/**
* Signatures MUST contain a single <ds:Reference> containing a same-document reference to the ID
* attribute value of the root element of the assertion or protocol message being signed. For example, if the
* ID attribute value is "foo", then the URI attribute in the <ds:Reference> element MUST be "#foo".
*/

$references = $signature->getSignedInfo()->getReferences();
Assert::count($references, 1, "A signature needs to have exactly one Reference, %d found.");

$reference = array_pop($references);
Assert::notNull($reference->getURI(), "URI attribute not found.");

Assert::same($reference->getURI(), '#' . $this->getID());

$this->signature = $signature;
}


/**
* Make sure the given Reference points to the original XML given.
*/
private function validateReferenceUri(Reference $reference, DOMElement $xml): void
{
if (
in_array(
$this->signature->getSignedInfo()->getCanonicalizationMethod()->getAlgorithm(),
[
C::C14N_INCLUSIVE_WITH_COMMENTS,
C::C14N_EXCLUSIVE_WITH_COMMENTS,
],
)
&& !$reference->isXPointer()
) { // canonicalization with comments used, but reference wasn't an xpointer!
throw new ReferenceValidationFailedException('Invalid reference for canonicalization algorithm.');
}

$id = $this->getId();
$uri = $reference->getURI();

if (empty($uri) || $uri === '#xpointer(/)') { // same-document reference
Assert::true(
$xml->isSameNode($xml->ownerDocument->documentElement),
'Cannot use document reference when element is not the root of the document.',
ReferenceValidationFailedException::class,
);
} else { // short-name or scheme-based xpointer
Assert::notEmpty(
$id,
'Reference points to an element, but given element does not have an ID.',
ReferenceValidationFailedException::class,
);
Assert::oneOf(
$uri,
[
'#' . $id,
'#xpointer(id(' . $id . '))',
],
'Reference does not point to given element.',
ReferenceValidationFailedException::class,
);
}
}


/**
* @param \SimpleSAML\XMLSecurity\XML\ds\SignedInfo $signedInfo
* @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface
*/
private function validateReference(SignedInfo $signedInfo): SignedElementInterface
{
$references = $signedInfo->getReferences();
Assert::count(
$references,
1,
'Exactly one reference expected in signature.',
TooManyElementsException::class,
);
$reference = array_pop($references);

$xml = $this->getOriginalXML();
$this->validateReferenceUri($reference, $xml);

$xp = XPath::getXPath($xml->ownerDocument);
$sigNode = XPath::xpQuery($xml, 'child::ds:Signature', $xp);
Assert::minCount($sigNode, 1, NoSignatureFoundException::class);
Assert::maxCount($sigNode, 1, 'More than one signature found in object.', TooManyElementsException::class);
$xml->removeChild($sigNode[0]);

$data = XML::processTransforms($reference->getTransforms(), $xml);
$algo = $reference->getDigestMethod()->getAlgorithm();
Assert::keyExists(
C::$DIGEST_ALGORITHMS,
$algo,
'Unsupported digest method "' . $algo . '"',
InvalidArgumentException::class,
);

$digest = hash(C::$DIGEST_ALGORITHMS[$algo], $data, true);
if (hash_equals($digest, base64_decode($reference->getDigestValue()->getRawContent(), true)) !== true) {
throw new SignatureVerificationFailedException('Failed to verify signature.');
}

$verifiedXml = DOMDocumentFactory::fromString($data);
return static::fromXML($verifiedXml->documentElement);
}


/**
* Verify this element against a public key.
*
* true is returned on success, false is returned if we don't have any
* signature we can verify. An exception is thrown if the signature
* validation fails.
*
* @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface|null $verifier The verifier to use to
* verify the signature. If null, attempt to verify it with the KeyInfo information in the signature.
*
* @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The Signed element if it was verified.
*/
private function verifyInternal(SignatureAlgorithmInterface $verifier): SignedElementInterface
{
/** @var \SimpleSAML\XMLSecurity\XML\ds\Signature $this->signature */
$signedInfo = $this->signature->getSignedInfo();
$c14nAlg = $signedInfo->getCanonicalizationMethod()->getAlgorithm();

// the canonicalized ds:SignedInfo element (plaintext)
$c14nSignedInfo = $signedInfo->canonicalize($c14nAlg);
$ref = $this->validateReference(
SignedInfo::fromXML(DOMDocumentFactory::fromString($c14nSignedInfo)->documentElement),
);

if (
$verifier?->verify(
$c14nSignedInfo, // the canonicalized ds:SignedInfo element (plaintext)
base64_decode($this->signature->getSignatureValue()->getRawContent(), true), // the actual signature
)
) {
/*
* validateReference() returns an object of the same class using this trait. This means the validatingKey
* property is available, and we can set it on the newly created object because we are in the same class,
* even thought the property itself is private.
*/
$ref->validatingKey = $verifier->getKey();
return $ref;
}
throw new SignatureVerificationFailedException('Failed to verify signature.');
}


/**
* Retrieve certificates that sign this element.
*
* @return \SimpleSAML\XMLSecurity\Key\KeyInterface|null The key that successfully verified this signature.
*/
public function getVerifyingKey(): ?KeyInterface
{
return $this->validatingKey;
}


/**
* Whether this object is signed or not.
*
* @return bool
*/
public function isSigned(): bool
{
return $this->signature !== null;
}


/**
* Verify the signature in this object.
*
* If no signature is present, false is returned. If a signature is present,
* but cannot be verified, an exception will be thrown.
*
* @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface|null $verifier The verifier to use to
* verify the signature. If null, attempt to verify it with the KeyInfo information in the signature.
* @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The object processed again from its canonicalised
* representation verified by the signature.
* @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException if the object is not signed.
* @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException if no key is passed and there is no KeyInfo
* in the signature.
* @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException if the signature fails to verify.
*/
public function verify(SignatureAlgorithmInterface $verifier = null): SignedElementInterface
{
if (!$this->isSigned()) {
throw new NoSignatureFoundException();
}

$keyInfo = $this->signature?->getKeyInfo();
$algId = $this->signature->getSignedInfo()->getSignatureMethod()->getAlgorithm();
if ($verifier === null && $keyInfo === null) {
throw new InvalidArgumentException('No key or KeyInfo available for signature verification.');
}

if ($verifier !== null) {
// verify using given key
// TODO: make this part of the condition, so that we support using this verifier to decrypt an encrypted key
Assert::eq(
$verifier->getAlgorithmId(),
$algId,
'Algorithm provided in key does not match algorithm used in signature.',
);

return $this->verifyInternal($verifier);
}

$factory = new SignatureAlgorithmFactory();
foreach ($keyInfo->getInfo() as $info) {
if (!$info instanceof X509Data) {
continue;
}

foreach ($info->getData() as $data) {
if (!$data instanceof X509Certificate) {
// not supported
continue;
}

// build a valid PEM for the certificate
$cert = sprintf(
"-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----",
$data->getRawContent(),
);

$cert = new Key\X509Certificate(PEM::fromString($cert));
$verifier = $factory->getAlgorithm($algId, $cert->getPublicKey());

try {
return $this->verifyInternal($verifier);
} catch (RuntimeException) {
// failed to verify with this certificate, try with other, if any
}
}
}
throw new SignatureVerificationFailedException('Failed to verify signature.');
}


/**
* @return string|null
*/
abstract public function getId(): ?string;


/**
* Get the list of algorithms that are blacklisted for any signing operation.
*
* @return string[]|null An array with all algorithm identifiers that are blacklisted, or null if we want to use the
* defaults.
*/
abstract public function getBlacklistedAlgorithms(): ?array;
}
2 changes: 1 addition & 1 deletion src/SAML2/XML/md/AbstractSignedMdElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

use DOMElement;
use SimpleSAML\SAML2\Compat\ContainerSingleton;
use SimpleSAML\SAML2\XML\SignedElementTrait;
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;

use function method_exists;

Expand Down
2 changes: 1 addition & 1 deletion src/SAML2/XML/saml/Assertion.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use SimpleSAML\SAML2\Constants as C;
use SimpleSAML\SAML2\Exception\ProtocolViolationException;
use SimpleSAML\SAML2\Utils\XPath;
use SimpleSAML\SAML2\XML\SignedElementTrait;
use SimpleSAML\XML\Exception\InvalidDOMElementException;
use SimpleSAML\XML\Exception\MissingElementException;
use SimpleSAML\XML\Exception\TooManyElementsException;
Expand All @@ -22,7 +23,6 @@
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;

use function array_filter;
use function array_merge;
Expand Down
2 changes: 1 addition & 1 deletion src/SAML2/XML/samlp/AbstractMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
use SimpleSAML\SAML2\Utils\XPath;
use SimpleSAML\SAML2\XML\ExtendableElementTrait;
use SimpleSAML\SAML2\XML\saml\Issuer;
use SimpleSAML\SAML2\XML\SignedElementTrait;
use SimpleSAML\XML\Utils\Random as RandomUtils;
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;

use function array_pop;

Expand Down

0 comments on commit 7f32ac3

Please sign in to comment.