diff --git a/src/SAML2/XML/SignedElementTrait.php b/src/SAML2/XML/SignedElementTrait.php new file mode 100644 index 000000000..e6dad802b --- /dev/null +++ b/src/SAML2/XML/SignedElementTrait.php @@ -0,0 +1,333 @@ +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 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 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; +} diff --git a/src/SAML2/XML/md/AbstractSignedMdElement.php b/src/SAML2/XML/md/AbstractSignedMdElement.php index 2f06cc67a..07419daf0 100644 --- a/src/SAML2/XML/md/AbstractSignedMdElement.php +++ b/src/SAML2/XML/md/AbstractSignedMdElement.php @@ -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; diff --git a/src/SAML2/XML/saml/Assertion.php b/src/SAML2/XML/saml/Assertion.php index cf52c843b..d646e7b64 100644 --- a/src/SAML2/XML/saml/Assertion.php +++ b/src/SAML2/XML/saml/Assertion.php @@ -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; @@ -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; diff --git a/src/SAML2/XML/samlp/AbstractMessage.php b/src/SAML2/XML/samlp/AbstractMessage.php index 5b1ce06e6..7fa6627a7 100644 --- a/src/SAML2/XML/samlp/AbstractMessage.php +++ b/src/SAML2/XML/samlp/AbstractMessage.php @@ -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;