Skip to content

Commit 20bcbd0

Browse files
committed
Implement signature verification, refactor signing
This commit adds support for verifying signatures (and renames the API method to verify(), which is the correct term we should us in this context, instead of validate()). It also refactors the way we sign, and adds support for canonicalization algorithms with comments, which were in practice not supported before. Signed-off-by: Jaime Pérez <jaimepc@gmail.com>
1 parent 4073c66 commit 20bcbd0

File tree

6 files changed

+685
-128
lines changed

6 files changed

+685
-128
lines changed

src/XML/SignableElementTrait.php

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace SimpleSAML\XMLSecurity\XML;
66

77
use DOMElement;
8-
use DOMNode;
98
use SimpleSAML\Assert\Assert;
9+
use SimpleSAML\XML\DOMDocumentFactory;
1010
use SimpleSAML\XMLSecurity\Alg\SignatureAlgorithm;
1111
use SimpleSAML\XMLSecurity\Constants as C;
1212
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
@@ -89,10 +89,69 @@ public function sign(
8989

9090

9191
/**
92-
* @param \DOMElement $xml
93-
* @throws \Exception
92+
* Get a ds:Reference pointing to this object.
93+
*
94+
* @param string $digestAlg The digest algorithm to use.
95+
* @param \SimpleSAML\XMLSecurity\XML\ds\Transforms $transforms The transforms to apply to the object.
9496
*/
95-
private function doSign(DOMElement $xml): void
97+
private function getReference(
98+
string $digestAlg,
99+
Transforms $transforms,
100+
DOMElement $xml,
101+
string $canonicalDocument
102+
): Reference {
103+
$id = $this->getId();
104+
$uri = null;
105+
if (empty($id)) { // document reference
106+
Assert::notNull(
107+
$xml->ownerDocument->documentElement,
108+
'Cannot create a document reference without a root element in the document.',
109+
RuntimeException::class
110+
);
111+
Assert::true(
112+
$xml->isSameNode($xml->ownerDocument->documentElement),
113+
'Cannot create a document reference when signing an object that is not the root of the document. ' .
114+
'Please give your object an identifier.',
115+
RuntimeException::class
116+
);
117+
if (in_array($this->c14nAlg, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
118+
$uri = '#xpointer(/)';
119+
}
120+
} elseif (in_array($this->c14nAlg, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
121+
// regular reference, but must retain comments
122+
$uri = '#xpointer(id(' . $id . '))';
123+
} else { // regular reference, can ignore comments
124+
$uri = '#' . $id;
125+
}
126+
127+
return new Reference(
128+
new DigestMethod($digestAlg),
129+
new DigestValue(Security::hash($digestAlg, $canonicalDocument)),
130+
$transforms,
131+
null,
132+
null,
133+
$uri
134+
);
135+
}
136+
137+
138+
/**
139+
* Do the actual signing of the document.
140+
*
141+
* Note that this method does not insert the signature in the returned \DOMElement. The signature will be available
142+
* in $this->signature as a \SimpleSAML\XMLSecurity\XML\ds\Signature object, which can then be converted to XML
143+
* calling toXML() on it, passing the \DOMElement value returned here as a parameter. The resulting \DOMElement
144+
* can then be inserted in the position desired.
145+
*
146+
* E.g.:
147+
* $xml = // our XML to sign
148+
* $signedXML = $this->doSign($xml);
149+
* $signedXML->appendChild($this->signature->toXML($signedXML));
150+
*
151+
* @param \DOMElement $xml The element to sign.
152+
* @return \DOMElement The signed element, without the signature attached to it just yet.
153+
*/
154+
protected function doSign(DOMElement $xml): DOMElement
96155
{
97156
Assert::notNull(
98157
$this->signer,
@@ -108,38 +167,18 @@ private function doSign(DOMElement $xml): void
108167
new Transform($this->c14nAlg)
109168
]);
110169

111-
$refId = $this->getId();
112-
$reference = new Reference(
113-
new DigestMethod($digest),
114-
new DigestValue(Security::hash($digest, XML::processTransforms($transforms, $xml))),
115-
$transforms,
116-
null,
117-
null,
118-
($refId !== null) ? '#' . $refId : null
119-
);
170+
$canonicalDocument = XML::processTransforms($transforms, $xml);
120171

121172
$signedInfo = new SignedInfo(
122173
new CanonicalizationMethod($this->c14nAlg),
123174
new SignatureMethod($algorithm),
124-
[$reference]
175+
[$this->getReference($digest, $transforms, $xml, $canonicalDocument)]
125176
);
126177

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

130181
$this->signature = new Signature($signedInfo, new SignatureValue($signedData), $this->keyInfo);
131-
}
132-
133-
134-
/**
135-
* @param DOMElement $root
136-
* @param DOMNode $node
137-
* @param DOMElement $signature
138-
* @return DOMElement
139-
*/
140-
private function insertBefore(DOMElement $root, DOMNode $node, DOMElement $signature): DOMElement
141-
{
142-
$root->removeChild($signature);
143-
return $root->insertBefore($signature, $node);
182+
return DOMDocumentFactory::fromString($canonicalDocument)->documentElement;
144183
}
145184
}

src/XML/SignedElementInterface.php

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace SimpleSAML\XMLSecurity\XML;
66

7-
use SimpleSAML\XMLSecurity\XMLSecurityKey;
7+
use SimpleSAML\XMLSecurity\Alg\SignatureAlgorithm;
8+
use SimpleSAML\XMLSecurity\Key\AbstractKey;
9+
use SimpleSAML\XMLSecurity\XML\ds\Signature;
810

911
/**
1012
* An interface describing signed elements.
@@ -14,12 +16,40 @@
1416
interface SignedElementInterface extends CanonicalizableElementInterface
1517
{
1618
/**
17-
* Retrieve certificates that sign this element.
19+
* Get the ID of this element.
1820
*
19-
* @return array Array with certificates.
21+
* When this method returns null, the signature created for this object will reference the entire document.
22+
*
23+
* @return string|null The ID of this element, or null if we don't have one.
24+
*/
25+
public function getId(): ?string;
26+
27+
28+
/**
29+
* Retrieve the signature in this object, if any.
30+
*
31+
* @return Signature|null
32+
*/
33+
public function getSignature(): ?Signature;
34+
35+
36+
/**
37+
* Retrieve the key used to validate the signature in this object.
38+
*
39+
* Warning:
40+
*
41+
* @return \SimpleSAML\XMLSecurity\Key\AbstractKey The key that validated the signature in this object.
2042
* @throws \Exception if an error occurs while trying to extract the public key from a certificate.
2143
*/
22-
public function getValidatingCertificates(): array;
44+
public function getValidatingKey(): ?AbstractKey;
45+
46+
47+
/**
48+
* Whether this object is signed or not.
49+
*
50+
* @return bool
51+
*/
52+
public function isSigned(): bool;
2353

2454

2555
/**
@@ -28,8 +58,14 @@ public function getValidatingCertificates(): array;
2858
* If no signature is present, false is returned. If a signature is present,
2959
* but cannot be verified, an exception will be thrown.
3060
*
31-
* @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key we should check against.
32-
* @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The signed element if we can verify the signature.
61+
* @param \SimpleSAML\XMLSecurity\Alg\SignatureAlgorithm|null $verifier The verifier to use to verify the signature.
62+
* If null, attempt to verify it with the KeyInfo information in the signature.
63+
* @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The object processed again from its canonicalised
64+
* representation verified by the signature.
65+
* @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFound if the object is not signed.
66+
* @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException if no key is passed and there is no KeyInfo
67+
* in the signature.
68+
* @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException if the signature fails to validate.
3369
*/
34-
public function validate(XMLSecurityKey $key): SignedElementInterface;
70+
public function verify(SignatureAlgorithm $verifier = null): SignedElementInterface;
3571
}

0 commit comments

Comments
 (0)