1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace SimpleSAML\XMLSecurity\XML; |
||
6 | |||
7 | use DOMElement; |
||
8 | use SimpleSAML\Assert\Assert; |
||
9 | use SimpleSAML\XML\DOMDocumentFactory; |
||
10 | use SimpleSAML\XML\Exception\TooManyElementsException; |
||
11 | use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; |
||
12 | use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface; |
||
13 | use SimpleSAML\XMLSecurity\Constants as C; |
||
14 | use SimpleSAML\XMLSecurity\CryptoEncoding\PEM; |
||
15 | use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException; |
||
16 | use SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException; |
||
17 | use SimpleSAML\XMLSecurity\Exception\ReferenceValidationFailedException; |
||
18 | use SimpleSAML\XMLSecurity\Exception\RuntimeException; |
||
19 | use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException; |
||
20 | use SimpleSAML\XMLSecurity\Key; |
||
21 | use SimpleSAML\XMLSecurity\Key\KeyInterface; |
||
22 | use SimpleSAML\XMLSecurity\Utils\XML; |
||
23 | use SimpleSAML\XMLSecurity\Utils\XPath; |
||
24 | use SimpleSAML\XMLSecurity\XML\ds\Reference; |
||
25 | use SimpleSAML\XMLSecurity\XML\ds\Signature; |
||
26 | use SimpleSAML\XMLSecurity\XML\ds\SignedInfo; |
||
27 | use SimpleSAML\XMLSecurity\XML\ds\X509Certificate; |
||
28 | use SimpleSAML\XMLSecurity\XML\ds\X509Data; |
||
29 | |||
30 | use function base64_decode; |
||
31 | use function hash; |
||
32 | use function hash_equals; |
||
33 | use function in_array; |
||
34 | |||
35 | /** |
||
36 | * Helper trait for processing signed elements. |
||
37 | * |
||
38 | * @package simplesamlphp/xml-security |
||
39 | */ |
||
40 | trait SignedElementTrait |
||
41 | { |
||
42 | use CanonicalizableElementTrait; |
||
43 | |||
44 | /** |
||
45 | * The signature of this element. |
||
46 | * |
||
47 | * @var \SimpleSAML\XMLSecurity\XML\ds\Signature|null $signature |
||
48 | */ |
||
49 | protected ?Signature $signature = null; |
||
50 | |||
51 | /** |
||
52 | * The key that successfully verifies the signature in this object. |
||
53 | * |
||
54 | * @var \SimpleSAML\XMLSecurity\Key\KeyInterface|null |
||
55 | */ |
||
56 | private ?KeyInterface $validatingKey = null; |
||
57 | |||
58 | |||
59 | /** |
||
60 | * Get the signature element of this object. |
||
61 | * |
||
62 | * @return \SimpleSAML\XMLSecurity\XML\ds\Signature |
||
63 | */ |
||
64 | public function getSignature(): ?Signature |
||
65 | { |
||
66 | return $this->signature; |
||
67 | } |
||
68 | |||
69 | |||
70 | /** |
||
71 | * Initialize a signed element from XML. |
||
72 | * |
||
73 | * @param \SimpleSAML\XMLSecurity\XML\ds\Signature $signature The ds:Signature object |
||
74 | */ |
||
75 | protected function setSignature(Signature $signature): void |
||
76 | { |
||
77 | /** |
||
78 | * By disabling formatting on signed objects, we prevent issues with invalid signatures later on |
||
79 | * at the cost of some whitespace taking up useless bytes in the outputted document. |
||
80 | */ |
||
81 | $this->formatOutput = false; |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
82 | $this->signature = $signature; |
||
83 | } |
||
84 | |||
85 | |||
86 | /** |
||
87 | * Make sure the given Reference points to the original XML given. |
||
88 | */ |
||
89 | private function validateReferenceUri(Reference $reference, DOMElement $xml): void |
||
90 | { |
||
91 | if ( |
||
92 | in_array( |
||
93 | $this->getSignature()->getSignedInfo()->getCanonicalizationMethod()->getAlgorithm(), |
||
94 | [ |
||
95 | C::C14N_INCLUSIVE_WITH_COMMENTS, |
||
96 | C::C14N_EXCLUSIVE_WITH_COMMENTS, |
||
97 | ], |
||
98 | ) |
||
99 | && !$reference->isXPointer() |
||
100 | ) { // canonicalization with comments used, but reference wasn't an xpointer! |
||
101 | throw new ReferenceValidationFailedException('Invalid reference for canonicalization algorithm.'); |
||
102 | } |
||
103 | |||
104 | $id = $this->getId(); |
||
105 | $uri = $reference->getURI(); |
||
106 | |||
107 | if (empty($uri) || $uri === '#xpointer(/)') { // same-document reference |
||
108 | Assert::true( |
||
109 | $xml->isSameNode($xml->ownerDocument->documentElement), |
||
110 | 'Cannot use document reference when element is not the root of the document.', |
||
111 | ReferenceValidationFailedException::class, |
||
112 | ); |
||
113 | } else { // short-name or scheme-based xpointer |
||
114 | Assert::notEmpty( |
||
115 | $id, |
||
116 | 'Reference points to an element, but given element does not have an ID.', |
||
117 | ReferenceValidationFailedException::class, |
||
118 | ); |
||
119 | Assert::oneOf( |
||
120 | $uri, |
||
121 | [ |
||
122 | '#' . $id, |
||
123 | '#xpointer(id(' . $id . '))', |
||
124 | ], |
||
125 | 'Reference does not point to given element.', |
||
126 | ReferenceValidationFailedException::class, |
||
127 | ); |
||
128 | } |
||
129 | } |
||
130 | |||
131 | |||
132 | /** |
||
133 | * @param \SimpleSAML\XMLSecurity\XML\ds\SignedInfo $signedInfo |
||
134 | * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface |
||
135 | */ |
||
136 | private function validateReference(SignedInfo $signedInfo): SignedElementInterface |
||
137 | { |
||
138 | $xml = $this->getOriginalXML(); |
||
139 | $references = $signedInfo->getReferences(); |
||
140 | foreach ($references as $reference) { |
||
141 | $this->validateReferenceUri($reference, $xml); |
||
142 | } |
||
143 | |||
144 | // Clone the document so we don't mess up the original DOMDocument |
||
145 | $doc = DOMDocumentFactory::create(); |
||
146 | $node = $doc->importNode($xml->ownerDocument->documentElement, true); |
||
147 | $doc->appendChild($node); |
||
148 | |||
149 | $xp = XPath::getXPath($doc); |
||
150 | $sigNode = XPath::xpQuery($doc->documentElement, 'child::ds:Signature', $xp); |
||
151 | Assert::minCount($sigNode, 1, NoSignatureFoundException::class); |
||
152 | Assert::maxCount($sigNode, 1, 'More than one signature found in object.', TooManyElementsException::class); |
||
153 | |||
154 | $doc->documentElement->removeChild($sigNode[0]); |
||
155 | $data = XML::processTransforms($reference->getTransforms(), $doc->documentElement); |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
156 | $algo = $reference->getDigestMethod()->getAlgorithm(); |
||
157 | Assert::keyExists( |
||
158 | C::$DIGEST_ALGORITHMS, |
||
159 | $algo, |
||
160 | 'Unsupported digest method "' . $algo . '"', |
||
161 | InvalidArgumentException::class, |
||
162 | ); |
||
163 | |||
164 | $digest = hash(C::$DIGEST_ALGORITHMS[$algo], $data, true); |
||
165 | if (hash_equals($digest, base64_decode($reference->getDigestValue()->getRawContent(), true)) !== true) { |
||
166 | throw new SignatureVerificationFailedException('Failed to verify signature.'); |
||
167 | } |
||
168 | |||
169 | $verifiedXml = DOMDocumentFactory::fromString($data); |
||
170 | return static::fromXML($verifiedXml->documentElement); |
||
171 | } |
||
172 | |||
173 | |||
174 | /** |
||
175 | * Verify this element against a public key. |
||
176 | * |
||
177 | * true is returned on success, false is returned if we don't have any |
||
178 | * signature we can verify. An exception is thrown if the signature |
||
179 | * validation fails. |
||
180 | * |
||
181 | * @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface|null $verifier The verifier to use to |
||
182 | * verify the signature. If null, attempt to verify it with the KeyInfo information in the signature. |
||
183 | * |
||
184 | * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The Signed element if it was verified. |
||
185 | */ |
||
186 | private function verifyInternal(SignatureAlgorithmInterface $verifier): SignedElementInterface |
||
187 | { |
||
188 | /** @var \SimpleSAML\XMLSecurity\XML\ds\Signature $this->signature */ |
||
189 | $signedInfo = $this->getSignature()->getSignedInfo(); |
||
190 | $c14nAlg = $signedInfo->getCanonicalizationMethod()->getAlgorithm(); |
||
191 | |||
192 | // the canonicalized ds:SignedInfo element (plaintext) |
||
193 | $c14nSignedInfo = $signedInfo->canonicalize($c14nAlg); |
||
194 | $ref = $this->validateReference( |
||
195 | SignedInfo::fromXML(DOMDocumentFactory::fromString($c14nSignedInfo)->documentElement), |
||
196 | ); |
||
197 | |||
198 | if ( |
||
199 | $verifier?->verify( |
||
200 | $c14nSignedInfo, // the canonicalized ds:SignedInfo element (plaintext) |
||
201 | // the actual signature |
||
202 | base64_decode($this->getSignature()->getSignatureValue()->getRawContent(), true), |
||
203 | ) |
||
204 | ) { |
||
205 | /* |
||
206 | * validateReference() returns an object of the same class using this trait. This means the validatingKey |
||
207 | * property is available, and we can set it on the newly created object because we are in the same class, |
||
208 | * even thought the property itself is private. |
||
209 | */ |
||
210 | $ref->validatingKey = $verifier->getKey(); |
||
0 ignored issues
–
show
|
|||
211 | return $ref; |
||
212 | } |
||
213 | throw new SignatureVerificationFailedException('Failed to verify signature.'); |
||
214 | } |
||
215 | |||
216 | |||
217 | /** |
||
218 | * Retrieve certificates that sign this element. |
||
219 | * |
||
220 | * @return \SimpleSAML\XMLSecurity\Key\KeyInterface|null The key that successfully verified this signature. |
||
221 | */ |
||
222 | public function getVerifyingKey(): ?KeyInterface |
||
223 | { |
||
224 | return $this->validatingKey; |
||
225 | } |
||
226 | |||
227 | |||
228 | /** |
||
229 | * Whether this object is signed or not. |
||
230 | * |
||
231 | * @return bool |
||
232 | */ |
||
233 | public function isSigned(): bool |
||
234 | { |
||
235 | return $this->signature !== null; |
||
236 | } |
||
237 | |||
238 | |||
239 | /** |
||
240 | * Verify the signature in this object. |
||
241 | * |
||
242 | * If no signature is present, false is returned. If a signature is present, |
||
243 | * but cannot be verified, an exception will be thrown. |
||
244 | * |
||
245 | * @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface|null $verifier The verifier to use to |
||
246 | * verify the signature. If null, attempt to verify it with the KeyInfo information in the signature. |
||
247 | * @return \SimpleSAML\XMLSecurity\XML\SignedElementInterface The object processed again from its canonicalised |
||
248 | * representation verified by the signature. |
||
249 | * @throws \SimpleSAML\XMLSecurity\Exception\NoSignatureFoundException if the object is not signed. |
||
250 | * @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException if no key is passed and there is no KeyInfo |
||
251 | * in the signature. |
||
252 | * @throws \SimpleSAML\XMLSecurity\Exception\RuntimeException if the signature fails to verify. |
||
253 | */ |
||
254 | public function verify(?SignatureAlgorithmInterface $verifier = null): SignedElementInterface |
||
255 | { |
||
256 | if (!$this->isSigned()) { |
||
257 | throw new NoSignatureFoundException(); |
||
258 | } |
||
259 | |||
260 | $keyInfo = $this->signature?->getKeyInfo(); |
||
261 | $algId = $this->signature->getSignedInfo()->getSignatureMethod()->getAlgorithm(); |
||
262 | if ($verifier === null && $keyInfo === null) { |
||
263 | throw new InvalidArgumentException('No key or KeyInfo available for signature verification.'); |
||
264 | } |
||
265 | |||
266 | if ($verifier !== null) { |
||
267 | // verify using given key |
||
268 | // TODO: make this part of the condition, so that we support using this verifier to decrypt an encrypted key |
||
269 | Assert::eq( |
||
270 | $verifier->getAlgorithmId(), |
||
271 | $algId, |
||
272 | 'Algorithm provided in key does not match algorithm used in signature.', |
||
273 | ); |
||
274 | |||
275 | return $this->verifyInternal($verifier); |
||
276 | } |
||
277 | |||
278 | $factory = new SignatureAlgorithmFactory(); |
||
279 | foreach ($keyInfo->getInfo() as $info) { |
||
280 | if (!$info instanceof X509Data) { |
||
281 | continue; |
||
282 | } |
||
283 | |||
284 | foreach ($info->getData() as $data) { |
||
285 | if (!$data instanceof X509Certificate) { |
||
286 | // not supported |
||
287 | continue; |
||
288 | } |
||
289 | |||
290 | // build a valid PEM for the certificate |
||
291 | $cert = sprintf( |
||
292 | "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", |
||
293 | $data->getRawContent(), |
||
294 | ); |
||
295 | |||
296 | $cert = new Key\X509Certificate(PEM::fromString($cert)); |
||
297 | $verifier = $factory->getAlgorithm($algId, $cert->getPublicKey()); |
||
298 | |||
299 | try { |
||
300 | return $this->verifyInternal($verifier); |
||
301 | } catch (RuntimeException) { |
||
302 | // failed to verify with this certificate, try with other, if any |
||
303 | } |
||
304 | } |
||
305 | } |
||
306 | throw new SignatureVerificationFailedException('Failed to verify signature.'); |
||
307 | } |
||
308 | |||
309 | |||
310 | /** |
||
311 | * @return string|null |
||
312 | */ |
||
313 | abstract public function getId(): ?string; |
||
314 | |||
315 | |||
316 | /** |
||
317 | * Get the list of algorithms that are blacklisted for any signing operation. |
||
318 | * |
||
319 | * @return string[]|null An array with all algorithm identifiers that are blacklisted, or null to use this |
||
320 | * libraries default. |
||
321 | */ |
||
322 | abstract public function getBlacklistedAlgorithms(): ?array; |
||
323 | } |
||
324 |