1: <?php
2: /**
3: * XMLDSigToken.php
4: *
5: * Copyright © 2017-2026 - Thierry Thiers <webcoder31@gmail.com>
6: *
7: * This software is governed by the CeCILL-C license under French law and
8: * abiding by the rules of distribution of free software. You can use, modify
9: * and/or redistribute the software under the terms of the CeCILL-C license as
10: * circulated by CEA, CNRS and INRIA at the following URL:
11: *
12: * http://www.cecill.info
13: *
14: * As a counterpart to the access to the source code and rights to copy, modify
15: * and redistribute granted by the license, users are provided only with a
16: * limited warranty and the software's author, the holder of the economic
17: * rights, and the successive licensors have only limited liability.
18: *
19: * In this respect, the user's attention is drawn to the risks associated with
20: * loading, using, modifying and/or developing or reproducing the software by
21: * the user in light of its specific status of free software, that may mean that
22: * it is complicated to manipulate, and that also therefore means that it is
23: * reserved for developers and experienced professionals having in-depth
24: * computer knowledge. Users are therefore encouraged to load and test the
25: * software's suitability as regards their requirements in conditions enabling
26: * the security of their systems and/or data to be ensured and, more generally,
27: * to use and operate it in the same conditions as regards security.
28: *
29: * The fact that you are presently reading this means that you have had
30: * knowledge of the CeCILL-C license and that you accept its terms.
31: *
32: * @author Thierry Thiers <webcoder31@gmail.com>
33: * @copyright 2017-2026 - Thierry Thiers <webcoder31@gmail.com>
34: * @license http://www.cecill.info CeCILL-C License
35: * @version 1.1.0
36: */
37:
38:
39: // Namespace.
40: namespace webcoder31\ezxmldsig;
41:
42: // Use.
43: use DateTime;
44: use DateTimeZone;
45: use DOMDocument;
46: use DOMElement;
47: use DOMNode;
48: use DOMXPath;
49: use Exception;
50: use RobRichards\XMLSecLibs\XMLSecurityDSig;
51: use RobRichards\XMLSecLibs\XMLSecEnc;
52: use RobRichards\XMLSecLibs\XMLSecurityKey;
53:
54: /**
55: * Build or analyze an XML token.
56: *
57: * An XML token is an enveloping XML Digital Signature (cf. RFC 3275)
58: * containing signed and timestamped user data. These data may also be
59: * encrypted. In this case we talk about Secure XML token.
60: *
61: * **This class uses the following default algorithms to operate:**
62: *
63: * - Algorithm used to canonicalize data before signing is:
64: * http://www.w3.org/TR/2001/REC-xml-c14n-20010315.
65: *
66: * - Algorithm used to generate the signature is:
67: * http://www.w3.org/2001/04/xmldsig-more#rsa-sha256.
68: *
69: * - Algorithm used to compute the hash of token data is:
70: * http://www.w3.org/2001/04/xmlenc#sha256.
71: *
72: * - Algorithm used to encode data's values is: Base64.
73: *
74: * The analysis expects these algorithms unless its options name others (see
75: * `__construct()`), and it requires a trust anchor: the CA certificates
76: * and/or the pinned certificate the signing certificate is verified against.
77: * Without it, no token is valid.
78: *
79: * **An XML token looks like this:**
80: *
81: * ```xml
82: * <?xml version="1.0"?>
83: * <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
84: * <ds:SignedInfo>
85: * <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
86: * <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
87: * <ds:Reference URI="#Token">
88: * <ds:Transforms>
89: * <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
90: * </ds:Transforms>
91: * <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
92: * <ds:DigestValue>---- TOKEN HASH ----</ds:DigestValue>
93: * </ds:Reference>
94: * </ds:SignedInfo>
95: * <ds:SignatureValue>---- SIGNATURE VALUE ----</ds:SignatureValue>
96: * <ds:KeyInfo>
97: * <ds:X509Data>
98: * <ds:X509Certificate>---- X.509 CERTIFICATE ----</ds:X509Certificate>
99: * </ds:X509Data>
100: * </ds:KeyInfo>
101: * <ds:Object Id="Token">
102: * <Token>
103: * <TokenTimestamp>2026-10-17T04:43:05Z</TokenTimestamp>
104: * <TokenId>---- 128-BIT RANDOM IDENTIFIER (HEXADECIMAL) ----</TokenId>
105: * <TokenData>
106: * <data1 Algorithm="base64">---- BASE64 ENCODED DATA ----</data1>
107: * <data2 Algorithm="base64">---- BASE64 ENCODED DATA ----</data2>
108: * <data3>
109: * <data31 Algorithm="base64">---- BASE64 ENCODED DATA ----</data31>
110: * <data32 Algorithm="base64">---- BASE64 ENCODED DATA ----</data32>
111: * </data3>
112: * <data4 Algorithm="base64">---- BASE64 ENCODED DATA ----</data4>
113: * </TokenData>
114: * </Token>
115: * </ds:Object>
116: * </ds:Signature>
117: * ```
118: *
119: * **The signing process consists of the following actions:**
120: *
121: * - Load data from an associative array.
122: *
123: * - Encode data values using Base64.
124: *
125: * - Build a node from the encoded array of data (cf. node `<TokenData />`).
126: *
127: * - Compute an UTC timestamp and store it in a node `<TokenTimestamp />`.
128: *
129: * - Generate a random identifier of 128 bits (`random_bytes()`) and store
130: * it, in hexadecimal, in a node `<TokenId />`. Signed with the rest of the
131: * token, it lets the receiver refuse a token it has already accepted (see
132: * `getTokenId()`).
133: *
134: * - Aggregate the nodes `<TokenTimestamp />`, `<TokenId />` and
135: * `<TokenData />` inside a node `<Token />`.
136: *
137: * - Load the base DOM document representing an XML digital signature:
138: *
139: * ```xml
140: * <ds:Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
141: * <ds:SignedInfo>
142: * <ds:SignatureMethod />
143: * </ds:SignedInfo>
144: * </ds:Signature>
145: * ```
146: *
147: * - Create a node `<ds:Object Id="Token" />` and append it to the node
148: * `<ds:Signature />`.
149: *
150: * - Add the node `<Token />` to the node `<ds:Object Id="Token" />`.
151: *
152: * - NOTE: If data encryption is requested, this process will take place here
153: * (this will be described later).
154: *
155: * - Build a node `<ds:Reference URI="#Token" />` and append it to the node
156: * `<ds:SignedInfo />`. This consists of:
157: *
158: * - Canonicalizing the content of the node `<ds:Object Id="Token" />`.
159: *
160: * - Computing the hash (SHA-256 by default) of the canonicalized node and
161: * storing it in the node `<ds:DigestValue />`.
162: *
163: * - Include, in the XML token, the X.509 certificate associated to the
164: * private key that will be used to perform the signing (see the next steps
165: * below). This certificate will be used to perform the signature validation
166: * process, avoiding the need, for the receiver of the XML Digital Signature,
167: * to own it on its side.
168: *
169: * - Canonicalize the content of the node `<ds:SignedInfo />`.
170: *
171: * - Compute the signature of the canonicalized node using the private key
172: * dedicated to perform signing.
173: *
174: * - Store the result in the node `<ds:SignatureValue />`.
175: *
176: * - Save the DOM document in XML format.
177: *
178: * **The signature validation process consists of the following actions:**
179: *
180: * - Load the XML Digital Signature in a DOM document. A document with a
181: * document type declaration (DOCTYPE), or not encoded in UTF-8, is
182: * refused before it is parsed, and the parser never accesses the network
183: * (see `_loadXML()`).
184: *
185: * - Verify that the algorithms of the XML Digital Signature are the
186: * expected ones.
187: *
188: * - Compute the hash of the content of the node `<ds:Object Id="Token" />`
189: * with the digest algorithm.
190: *
191: * - Compare the computed hash with the one stored in the node
192: * `<ds:DigestValue />`. If the hashes are different, this means that the
193: * content of the node `<ds:Object Id="Token" />` has been altered.
194: *
195: * - Canonicalize the content of the node `<ds:SignedInfo />`.
196: *
197: * - Compute the signature of the node `<ds:SignedInfo />` using the X.509
198: * certificate included in the XML token.
199: *
200: * - Compare the computed signature with the one stored in the node
201: * `<ds:SignatureValue>`. If the signatures are different, this means that
202: * the content of the node `<ds:SignedInfo />` has not been signed using the
203: * private key associated to the X.509 certificate included in the XML
204: * Digital Signature.
205: *
206: * - Verify that the X.509 certificate included in the XML Digital Signature
207: * is trusted, against the trust anchor given to the analysis: CA
208: * certificates up to which OpenSSL verifies the chain of the certificate,
209: * and/or the pinned certificate the token must be signed with. Without a
210: * trust anchor, no token is valid: anyone can sign a token with a
211: * certificate of their own.
212: *
213: * - NOTE: If data decryption is required, this process will take place here
214: * (this will be described later).
215: *
216: * - Extract the timestamp and the identifier stored in the content of the
217: * node `<ds:Object Id="Token" />` and memorise them. A token without
218: * identifier is rejected.
219: *
220: * - Extract the data stored in the content of the node
221: * `<ds:Object Id="Token" />`.
222: *
223: * - Decode data values using Base64.
224: *
225: * - Build an associative array containing the decoded data.
226: *
227: * **Token data encryption / decryption:**
228: *
229: * The token data may also be encrypted before signing. In this case the class
230: * will use the following default algorithms to perform encryption and
231: * decryption of data:
232: *
233: * - Algorithm used to encrypt / decrypt data is:
234: * http://www.w3.org/2009/xmlenc11#aes256-gcm (authenticated symmetric
235: * ciphering).
236: *
237: * - Algorithm used to encrypt / decrypt the session key is:
238: * http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p (asymmetric ciphering).
239: *
240: * **A secure (encrypted) XML token looks like this:**
241: *
242: * ```xml
243: * <?xml version="1.0"?>
244: * <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
245: * <ds:SignedInfo>
246: * <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
247: * <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
248: * <ds:Reference URI="#Token">
249: * <ds:Transforms>
250: * <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
251: * </ds:Transforms>
252: * <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
253: * <ds:DigestValue>---- TOKEN HASH ----</ds:DigestValue>
254: * </ds:Reference>
255: * </ds:SignedInfo>
256: * <ds:SignatureValue>---- SIGNATURE VALUE ----</ds:SignatureValue>
257: * <ds:KeyInfo>
258: * <ds:X509Data>
259: * <ds:X509Certificate>---- X.509 CERTIFICATE ----</ds:X509Certificate>
260: * </ds:X509Data>
261: * </ds:KeyInfo>
262: * <ds:Object Id="Token">
263: * <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Content">
264: * <xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes256-gcm"/>
265: * <ds:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
266: * <xenc:EncryptedKey>
267: * <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
268: * <xenc:CipherData>
269: * <xenc:CipherValue>---- CIPHERED SESSION KEY ----</xenc:CipherValue>
270: * </xenc:CipherData>
271: * </xenc:EncryptedKey>
272: * </ds:KeyInfo>
273: * <xenc:CipherData>
274: * <xenc:CipherValue>---- ENCRYPTED DATA ----</xenc:CipherValue>
275: * </xenc:CipherData>
276: * </xenc:EncryptedData>
277: * </ds:Object>
278: * </ds:Signature>
279: * ```
280: *
281: * **The encryption process is the following:**
282: *
283: * - Randomly generate a session key.
284: *
285: * - Cipher the session key using the X.509 certificate dedicated to the
286: * encryption process, in order to transmit it safely in the XML Digital
287: * Signature.
288: *
289: * - Store the ciphered session key in the node `<xenc:CipherValue />` which is
290: * located inside the node `<xenc:EncryptedKey />`.
291: *
292: * - Encrypt the content of the node `<ds:Object Id="Token" />` using the
293: * non-ciphered session key and store the result in the last node
294: * `<xenc:CipherValue />`.
295: *
296: * **The decryption process is the following:**
297: *
298: * - Retrieve the node `<xenc:EncryptedData />`.
299: *
300: * - Decipher the ciphered session key stored in the node `<xenc:CipherValue />`
301: * (the one which is located inside the node `<xenc:EncryptedKey />`) using
302: * the private key associated to the X.509 certificate that has been used to
303: * perform the encryption.
304: *
305: * - Decrypt the content of the last node `<xenc:CipherValue />` using the
306: * deciphered session key.
307: *
308: * The decryption only takes place once the digest, the signature, the trust
309: * in the signing certificate and the algorithms have been verified, and
310: * every failure that depends on the encrypted content gives the same error,
311: * `Token decryption failed!`. RSA 1.5 is refused, and a CBC algorithm is
312: * only accepted when the analysis declares it (see `__construct()`).
313: *
314: * **Replay:**
315: *
316: * A valid token can be submitted again for as long as `isOutOfDate()`
317: * accepts it. The class keeps no list of the tokens it has analyzed: the
318: * caller must keep the identifiers it accepted (`getTokenId()`) and refuse a
319: * token whose identifier is among them.
320: */
321: class XMLDSigToken
322: {
323: /* CONSTANTS */
324:
325: /** @const string The default XMLDSIG namespace prefix used in the XML digital
326: signature. */
327: const XMLDSIG_NS_PREFIX = 'ds';
328:
329: /** @const string The name of the token node. */
330: const TOKEN_NAME = 'Token';
331:
332: /** @const string The name of the token data node. */
333: const TOKEN_DATA_NAME = 'TokenData';
334:
335: /** @const string The name of the token timestamp node. */
336: const TOKEN_TIMESTAMP_NAME = 'TokenTimestamp';
337:
338: /** @const string The name of the token identifier node. */
339: const TOKEN_ID_NAME = 'TokenId';
340:
341: /** @const int The number of random bytes of the token identifier, written
342: in hexadecimal (see getTokenId()). */
343: const TOKEN_ID_BYTES = 16;
344:
345: /** @const string The name of the node that holds one audience of the
346: token (see getAudience()). A token holds one such node per audience. */
347: const TOKEN_AUDIENCE_NAME = 'TokenAudience';
348:
349: /** @const string The name of the token expiry node (see getExpiry()). */
350: const TOKEN_EXPIRY_NAME = 'TokenExpiry';
351:
352: /** @const int The latest expiry the format of the timestamp can write
353: (9999-12-31T23:59:59Z), as a UNIX timestamp. */
354: private const MAX_EXPIRY_TIME = 253402300799;
355:
356: /** @const string The format of the token timestamp (YYYY-MM-DDThh:mm:ssZ).
357: NOTE: The timestamp should always be expressed in UTC. */
358: const TOKEN_TIMESTAMP_FORMAT = 'Y-m-d\TH:i:s\Z';
359:
360: /** @const int The default time to live (in seconds) used to check token
361: peremption. */
362: const TOKEN_DEFAULT_TTL = 60;
363:
364: /** @const int The synchronization offset (in seconds) allowed for verifying
365: if token is out of date. */
366: const DESYNC_TOLERANCE = 30;
367:
368:
369: const C14N = XMLSecurityDSig::C14N;
370: const C14N_COMMENTS = XMLSecurityDSig::C14N_COMMENTS;
371: const EXC_C14N = XMLSecurityDSig::EXC_C14N;
372: const EXC_C14N_COMMENTS = XMLSecurityDSig::EXC_C14N_COMMENTS;
373:
374: const SHA1 = XMLSecurityDSig::SHA1;
375: const SHA256 = XMLSecurityDSig::SHA256;
376: const SHA384 = XMLSecurityDSig::SHA384;
377: const SHA512 = XMLSecurityDSig::SHA512;
378: const RIPEMD160 = XMLSecurityDSig::RIPEMD160;
379:
380: const TRIPLEDES_CBC = XMLSecurityKey::TRIPLEDES_CBC;
381: const AES128_CBC = XMLSecurityKey::AES128_CBC;
382: const AES192_CBC = XMLSecurityKey::AES192_CBC;
383: const AES256_CBC = XMLSecurityKey::AES256_CBC;
384: const AES128_GCM = XMLSecurityKey::AES128_GCM;
385: const AES192_GCM = XMLSecurityKey::AES192_GCM;
386: const AES256_GCM = XMLSecurityKey::AES256_GCM;
387: const RSA_OAEP_MGF1P = XMLSecurityKey::RSA_OAEP_MGF1P;
388: const DSA_SHA1 = XMLSecurityKey::DSA_SHA1;
389: const RSA_SHA1 = XMLSecurityKey::RSA_SHA1;
390: const RSA_SHA256 = XMLSecurityKey::RSA_SHA256;
391: const RSA_SHA384 = XMLSecurityKey::RSA_SHA384;
392: const RSA_SHA512 = XMLSecurityKey::RSA_SHA512;
393: const HMAC_SHA1 = XMLSecurityKey::HMAC_SHA1;
394:
395: /** @const array The context each option applies to (see
396: _checkOptions()). */
397: private const OPTION_CONTEXTS = [
398: 'base64Encode' => 'creation',
399: 'xmldsigNsPrefix' => 'creation',
400: 'audience' => 'creation',
401: 'validity' => 'creation',
402: 'expectedAudience' => 'analysis',
403: 'canonicalizationAlgorithm' => 'any',
404: 'signatureAlgorithm' => 'any',
405: 'digestAlgorithm' => 'any',
406: 'checkSigningAlgorithms' => 'analysis',
407: 'caCertPath' => 'analysis',
408: 'signerCertPath' => 'analysis',
409: 'expectedSubject' => 'analysis',
410: 'sessionKeyCipheringAlgorithm' => 'encryption',
411: 'cryptAlgorithm' => 'encryption',
412: 'checkCryptingAlgorithms' => 'decryption',
413: ];
414:
415: /** @const string The error of every failure that depends on the
416: decrypted content (see _decryptToken()). */
417: private const DECRYPTION_FAILED = 'Token decryption failed!';
418:
419:
420: /* PROPERTIES */
421:
422: /** @var string The XML namespace (xmlns) prefix to use in the XML digital
423: signature tags. */
424: private $xmldsigNsPrefix = self::XMLDSIG_NS_PREFIX;
425:
426: /** @var string The algorithm that should be used to canonicalize the node
427: "ds:Signature/ds:SignedInfo" of the XML digital signature before signing
428: it. */
429: private $canonicalizationAlgorithm = self::C14N;
430:
431: /** @var string The algorithm that should be used to sign the computed hash
432: of the token. */
433: private $signatureAlgorithm = self::RSA_SHA256;
434:
435: /** @var string The algorithm that should be used to compute the hash of the
436: token. */
437: private $digestAlgorithm = self::SHA256;
438:
439: /** @var string The algorithm that should be used to canonicalize the token
440: before computing its hash. NOTE: In our case, it is the same algorithm as
441: the one used to canonicalize the node "ds:Signature/ds:signedInfo"
442: (see: $canonicalizationAlgorithm property). */
443: private $transformAlgorithm = self::C14N;
444:
445: /** @var boolean Whether to check the algorithms actually used (read from
446: the XML digital signature) to sign the token or not. */
447: private $checkSigningAlgorithms = true;
448:
449: /** @var string The algorithm that should be used to generate the session
450: key which will be used to encrypt the token. */
451: private $sessionKeyCipheringAlgorithm = self::AES256_GCM;
452:
453: /** @var string The algorithm that should be used to encrypt the token with
454: the generated session key. */
455: private $cryptAlgorithm = self::RSA_OAEP_MGF1P;
456:
457: /** @var boolean Whether to check the algorithms actually used (read from
458: the XML digital signature) to encrypt the token or not. */
459: private $checkCryptingAlgorithms = true;
460:
461: /** @var DOMXPath The DOMXPath object used to navigate through the XML
462: digital signature enveloping the token. */
463: private $xpath = null;
464:
465: /** @var string The content of the private key used to sign the token
466: data. */
467: private $signKey = null;
468:
469: /** @var string The password to access the private key used to sign the
470: token data. */
471: private $signKeyPassword = null;
472:
473: /** @var string The content of the X.509 certificate associated to the
474: private key (cf. $signKey) used to sign the token. This certificate is
475: included in the XML token, in order to verify that the XML
476: Digital Signature is valid. */
477: private $signCert = null;
478:
479: /** @var string The content of the private key used to decrypt the token. */
480: private $cryptKey = null;
481:
482: /** @var string The password to access the private key used to decrypt the
483: token. */
484: private $cryptKeyPassword = null;
485:
486: /** @var string The content of the X.509 certificate associated to the
487: private key used to encrypt the session key used to encrypt the token. */
488: private $cryptCert = null;
489:
490: /** @var string The XML digital signature enveloping the token. */
491: private $xml = null;
492:
493: /** @var array The token data (a flat or multidimensional associative
494: array). */
495: private $data = null;
496:
497: /** @var boolean Flag that indicates whether the token data should be base64
498: encoded when building the XML token or not. */
499: private $base64EncodeData = true;
500:
501: /** @var string The token timestamp (format: 'yyyy-MM-ddThh:mm:ssZ'). */
502: private $timestamp = null;
503:
504: /** @var string The token identifier (see getTokenId()). */
505: private $tokenId = null;
506:
507: /** @var array|null The audiences of the token to create (option
508: "audience"). */
509: private $audienceToWrite = null;
510:
511: /** @var array|null The audiences read from the token (see
512: getAudience()). */
513: private $audience = null;
514:
515: /** @var int|null The validity of the token to create, in seconds (option
516: "validity"). */
517: private $validity = null;
518:
519: /** @var string|null The token expiry (format: 'yyyy-MM-ddThh:mm:ssZ', see
520: getExpiry()). */
521: private $expiry = null;
522:
523: /** @var boolean Whether the analyzed token has a node "TokenExpiry",
524: well formed or not (see isOutOfDate()). */
525: private $hasExpiryNode = false;
526:
527: /** @var string|null The audience the analyzed token must be addressed to
528: (option "expectedAudience"). */
529: private $expectedAudience = null;
530:
531: /** @var X509Cert The X.509 certificate included in the XML Digital
532: Signature. */
533: private $x509Certificate = null;
534:
535: /** @var array|null The paths of the PEM files holding the trusted CA
536: certificates (option "caCertPath"). */
537: private $trustedCACertPaths = null;
538:
539: /** @var string|null The pinned signing certificate, in PEM format (option
540: "signerCertPath"), or, when the object creates a token, the certificate
541: of its own signing key. */
542: private $pinnedSignerCert = null;
543:
544: /** @var array|string|null The subject expected for the signing
545: certificate (option "expectedSubject"). */
546: private $expectedSubject = null;
547:
548: /** @var boolean Flag that indicates whether the X.509 certificate included
549: in the XML Digital Signature is trusted or not. */
550: private $isCertTrusted = null;
551:
552: /** @var boolean Flag that indicates whether the token is encrypted or
553: not. */
554: private $isDataEncrypted = null;
555:
556: /** @var boolean Flag that indicates whether the token hash is valid or
557: not. */
558: private $isDigestValueOk = null;
559:
560: /** @var boolean Flag that indicates whether the token hash signature is
561: valid or not. */
562: private $isSignatureValueOk = null;
563:
564: /** @var string Error encountered during the analysis of
565: the XML digital signature enveloping the token. */
566: private $error = null;
567:
568: /** @var array List of the anomalies encountered during the analysis
569: of the XML digital signature enveloping the token. */
570: private $anomalies = array();
571:
572: /**
573: * Defines a custom output for `print_r()` and `var_dump()` functions,
574: * in order to hide cryptographic material used by the object for
575: * security reasons.
576: */
577: public function __debugInfo()
578: {
579: return [
580: 'isValid' => $this->isSignatureValid(),
581: 'isDigestValueOk' => $this->isDigestValueOk,
582: 'isSignatureValueOk' => $this->isSignatureValueOk,
583: 'isCertTrusted' => $this->isCertTrusted,
584: 'isDataEncrypted' => $this->isDataEncrypted,
585: 'timestamp' => $this->timestamp,
586: 'tokenId' => $this->getTokenId(),
587: 'audience' => $this->getAudience(),
588: 'expiry' => $this->getExpiry(),
589: 'data' => $this->data,
590: 'error' => $this->error,
591: 'anomalies' => $this->anomalies
592: ];
593: }
594:
595:
596: /**
597: * Instantiate an XMLDSigToken object.
598: *
599: * This constructor is `protected`. To get an instance of an XMLDSigToken
600: * object use one of the following `static` functions according to what the
601: * object is intended for:
602: *
603: * - `createXMLToken()`
604: *
605: * - `createSecureXMLToken()`
606: *
607: * - `analyzeXMLToken()`
608: *
609: * - `analyzeSecureXMLToken()`
610: *
611: * An XMLDSigToken object allows creating an XML token and also analyzing an
612: * existing XML token. The type of the `$xmlOrData` parameter determines how
613: * the object behaves.
614: *
615: * **XML token creation:**
616: *
617: * If `$xmlOrData` is an array (a flat or multidimensional associative
618: * array), the object will create an enveloping XML digital signature
619: * containing an XML token that holds the provided data and a timestamp
620: * generated automatically, with a random identifier (see `getTokenId()`).
621: * In this case, the `$signKeyPath` and `$signCertPath` parameters are
622: * required. If the `$cryptCertPath` parameter is also provided, the XML
623: * token will be encrypted for the owner of this certificate. The
624: * `$cryptKeyPath` parameter, the private key of the recipient, is
625: * optional: given, it is used to decrypt the created token and check its
626: * content too. If this private key is protected by a password, this
627: * password may be passed using the `$cryptKeyPassword` parameter.
628: *
629: * **XML token analysis:**
630: *
631: * If `$xmlOrData` is a string, the object will treat it as an enveloping
632: * XML digital signature containing a token:
633: *
634: * - It will check that the signature is valid.
635: *
636: * - It will decrypt the token if this one is encrypted.
637: *
638: * - It will extract timestamp and data from the token.
639: *
640: * In this case, the `$xmlOrData` parameter and a trust anchor (the
641: * options `caCertPath` and/or `signerCertPath`, see below) are required:
642: * without a trust anchor, no token is valid. If the XML
643: * token is encrypted, the `$cryptKeyPath` parameter must also be provided.
644: * If the private key used for decryption (cf. `$cryptKeyPath` parameter) is
645: * protected by a password, this password may be passed using the
646: * `$cryptKeyPassword` parameter.
647: *
648: * When the object creates a token, it analyzes it right away to check it,
649: * and trusts the certificate of its own signing key (`$signCertPath`) for
650: * that: the trust options do not apply to token creation. An encrypted
651: * token created without `$cryptKeyPath` is checked up to its decryption
652: * (structure, algorithms, digest, signature, trust in the signing
653: * certificate) and its content is not read (see `createSecureXMLToken()`).
654: *
655: * **Configuring the object:**
656: *
657: * The `$options` parameter of this function allows to override the default
658: * configuration of the object passing it the desired options via an
659: * associative array.
660: *
661: * Example:
662: *
663: * ```php
664: * $options = [
665: * 'base64Encode' => false,
666: * 'xmldsigNsPrefix' => ''
667: * ];
668: * ```
669: *
670: * Each option applies to a context: token creation (`createXMLToken()`,
671: * `createSecureXMLToken()`), token analysis (`analyzeXMLToken()`,
672: * `analyzeSecureXMLToken()`), or both. The encryption options only apply
673: * when an encryption certificate (creation) or a decryption key
674: * (analysis) is given. An unknown option, an option given out of its
675: * context, or an option whose value is NULL or invalid throws an
676: * exception.
677: *
678: * The algorithm options apply to both contexts: at creation, they are the
679: * algorithms the token is built with; at analysis, the algorithms the
680: * token must have been built with. A token built with other algorithms is
681: * rejected (`getError()` lists them), unless `checkSigningAlgorithms` or
682: * `checkCryptingAlgorithms` is FALSE. To analyze a token built with
683: * RSA-SHA1 and SHA-1:
684: *
685: * ```php
686: * $options = [
687: * 'caCertPath' => ['intermediate.cert.pem', 'root.cert.pem'],
688: * 'signatureAlgorithm' => XMLDSigToken::RSA_SHA1,
689: * 'digestAlgorithm' => XMLDSigToken::SHA1
690: * ];
691: * ```
692: *
693: * Available options are the following:
694: *
695: * - **`caCertPath`** [string|array] Analysis only. The path, or the list
696: * of paths, of PEM files holding the trusted CA certificates. The
697: * signing certificate is only trusted if OpenSSL builds and verifies
698: * its chain, from these certificates only, up to a self-signed
699: * certificate of these files (see `X509Cert::isTrustedBy()`). To trust
700: * the certificates issued by an intermediate CA, give its certificate
701: * and the certificates up to its root, in one file or in several:
702: * `['intermediate.cert.pem', 'root.cert.pem']`. A chain that stops at
703: * an intermediate CA is rejected: with the intermediate CA alone,
704: * `isSignatureValid()` returns FALSE and `getError()` returns "Signing
705: * certificate is not issued by a trusted CA: its chain could not be
706: * verified up to the CA certificates of the option 'caCertPath', which
707: * must hold the issuing CA and the certificates up to its root!".
708: * Only the signing certificate is taken from the token, so a
709: * certificate issued by another intermediate CA of the same root is
710: * rejected; a certificate issued directly by a CA of the files (the
711: * root included) is accepted.
712: * <br/>Default value: none.
713: *
714: * - **`signerCertPath`** [string] Analysis only. The path to the PEM
715: * certificate the token must be signed with (certificate pinning): the
716: * signing certificate must be this very certificate. Given alone, it is
717: * the trust anchor; given with `caCertPath`, both are required.
718: * <br/>Default value: none.
719: *
720: * - **`expectedSubject`** [string|array] Analysis only. The subject the
721: * signing certificate must have, either:
722: *
723: * - an array of the subject fields to check, as returned by
724: * `getCertSubject()`: `['O' => 'Lothbrok Ltd', 'OU' => 'Jarl Dept']`.
725: * Fields that are not given are not checked.
726: *
727: * - or a string, compared as is with the full Distinguished Name
728: * returned by `getCertDN()`. This is the OpenSSL "oneline" format
729: * (`openssl_x509_parse()`, key `name`): every field of the subject,
730: * in the order of the certificate, each written `/<short name>=<value>`
731: * with no space and no escaping, e.g.
732: * `'/C=DK/ST=Jylland/L=Kattegat/O=Lothbrok Ltd/OU=Signature Dept/CN=www.lothbrok.dk/emailAddress=ivar@lothbrok.dk'`.
733: * It is neither the RFC 2253 form (`CN=...,O=...`) nor the form
734: * printed by the `openssl` command line (`C=DK, ST=...`): copy it
735: * from `getCertDN()` or `X509Cert::getDN()`.
736: *
737: * Character case is significant. It does not replace a trust anchor.
738: * <br/>Default value: none.
739: *
740: * - **`expectedAudience`** [string] Analysis only. The audience of this
741: * receiver, which the token must be addressed to (see `audience`):
742: * `isSignatureValid()` returns FALSE if the token names no audience or
743: * does not name this one. A token that names audiences is rejected
744: * when this option is not given, so that a receiver of the issuer
745: * cannot accept a token addressed to another one. Compared as is
746: * (character case included).
747: * <br/>Default value: none.
748: *
749: * - **`audience`** [string|array] Creation only. The audience, or the
750: * list of audiences, the token is addressed to: names of the receivers
751: * (an application name, a URL...), agreed between the issuer and each
752: * receiver. Each one is a non-empty string, with no surrounding white
753: * space and no control character. They are written in the token,
754: * covered by the signature (see `getAudience()`).
755: * <br/>Default value: none (the token names no audience).
756: *
757: * - **`validity`** [int] Creation only. The number of seconds after which
758: * the token is out of date, whatever the time to live the receiver
759: * gives to `isOutOfDate()`. The expiry date is written in the token,
760: * covered by the signature (see `getExpiry()`).
761: * <br/>Default value: none (only the receiver sets the time to live).
762: *
763: * - **`base64Encode`** [boolean] Creation only. Whether to base64 encode
764: * token data or not.
765: * <br/>Default value: **`TRUE`**.
766: *
767: * - **`xmldsigNsPrefix`** [string] Creation only. The XML namespace
768: * (xmlns) prefix to use in the XML digital signature tags. Set it to
769: * empty string to avoid prefix usage.
770: * <br/>Default value: **`'ds'`**.
771: *
772: * - **`checkSigningAlgorithms`** [boolean] Analysis only. Whether to
773: * check that the algorithms used to sign the token are the expected
774: * ones or not. Even when FALSE, an algorithm that is not in the lists
775: * below is rejected.
776: * <br/>Default value: **`TRUE`**.
777: *
778: * - **`checkCryptingAlgorithms`** [boolean] Analysis with a decryption
779: * key only. Whether to check that the algorithms used to encrypt the
780: * token are the expected ones or not. Even when FALSE, an algorithm
781: * that is not in the lists below is rejected.
782: * <br/>Default value: **`TRUE`**.
783: *
784: * - **`canonicalizationAlgorithm`** [string] Creation and analysis. The
785: * algorithm used to canonicalize the XML digital signature and to
786: * canonicalize token data before computing its hash.
787: * <br/>Possible values:
788: *
789: * - **`C14N`** (Default value)
790: * <br/>cf. http://www.w3.org/TR/2001/REC-xml-c14n-20010315
791: *
792: * - `C14N_COMMENTS`
793: * <br/>cf. http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments
794: *
795: * - `EXC_C14N`
796: * <br/>cf. http://www.w3.org/2001/10/xml-exc-c14n#
797: *
798: * - `EXC_C14N_COMMENTS`
799: * <br/>cf. http://www.w3.org/2001/10/xml-exc-c14n#WithComments
800: *
801: * - **`signatureAlgorithm`** [string] Creation and analysis. The
802: * asymmetric algorithm used to sign the hash of token data. Key
803: * transport algorithms (see `cryptAlgorithm`) are rejected.
804: * <br/>Possible values:
805: *
806: * - `RSA_SHA1`
807: * <br/>cf. http://www.w3.org/2000/09/xmldsig#rsa-sha1
808: *
809: * - **`RSA_SHA256`** (Default value)
810: * <br/>cf. http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
811: *
812: * - `RSA_SHA384`
813: * <br/>cf. http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
814: *
815: * - `RSA_SHA512`
816: * <br/>cf. http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
817: *
818: * - **`digestAlgorithm`** [string] Creation and analysis. The algorithm
819: * used to compute the hash of token data.
820: * <br/>Possible values:
821: *
822: * - `SHA1`
823: * <br/>cf. http://www.w3.org/2000/09/xmldsig#sha1
824: *
825: * - **`SHA256`** (Default value)
826: * <br/>cf. http://www.w3.org/2001/04/xmlenc#sha256
827: *
828: * - `SHA384`
829: * <br/>cf. http://www.w3.org/2001/04/xmldsig-more#sha384
830: *
831: * - `SHA512`
832: * <br/>cf. http://www.w3.org/2001/04/xmlenc#sha512
833: *
834: * - `RIPEMD160`
835: * <br/>cf. http://www.w3.org/2001/04/xmlenc#ripemd160
836: *
837: * - **`sessionKeyCipheringAlgorithm`** [string] Encryption only
838: * (creation with an encryption certificate, analysis with a decryption
839: * key). The symmetric algorithm used to encrypt token data with a
840: * generated session key.
841: * <br/>Possible values:
842: *
843: * - `AES128_GCM`
844: * <br/>cf. http://www.w3.org/2009/xmlenc11#aes128-gcm
845: *
846: * - `AES192_GCM`
847: * <br/>cf. http://www.w3.org/2009/xmlenc11#aes192-gcm
848: *
849: * - **`AES256_GCM`** (Default value)
850: * <br/>cf. http://www.w3.org/2009/xmlenc11#aes256-gcm
851: *
852: * - `TRIPLEDES_CBC`
853: * <br/>cf. http://www.w3.org/2001/04/xmlenc#tripledes-cbc
854: *
855: * - `AES128_CBC`
856: * <br/>cf. http://www.w3.org/2001/04/xmlenc#aes128-cbc
857: *
858: * - `AES192_CBC`
859: * <br/>cf. http://www.w3.org/2001/04/xmlenc#aes192-cbc
860: *
861: * - `AES256_CBC`
862: * <br/>cf. http://www.w3.org/2001/04/xmlenc#aes256-cbc
863: *
864: * The CBC algorithms do not protect the integrity of the data: only
865: * the signature does. An analysis only decrypts a token encrypted with
866: * one of them if this option names it, even when
867: * `checkCryptingAlgorithms` is FALSE.
868: *
869: * - **`cryptAlgorithm`** [string] Encryption only (creation with an
870: * encryption certificate, analysis with a decryption key). The
871: * asymmetric algorithm used to encrypt the session key with the
872: * encryption certificate (key transport). Signature algorithms (see
873: * `signatureAlgorithm`) are rejected, and so is RSA 1.5
874: * (http://www.w3.org/2001/04/xmlenc#rsa-1_5), at creation as at
875: * analysis.
876: * <br/>Possible values:
877: *
878: * - **`RSA_OAEP_MGF1P`** (Default value)
879: * <br/>cf. http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p
880: *
881: * @param mixed $xmlOrData An XML token (string) or token data (associative
882: * array).
883: *
884: * @param string $signKeyPath The path to the PEM private key file that will
885: * be used to sign the token.
886: *
887: * @param string $signKeyPassword The password, if needed, to access the
888: * private key that will be used to sign the token. Use NULL if no
889: * password needed.
890: *
891: * @param string $signCertPath The path to the PEM X.509 certificate file
892: * that will be included in the XML digital signature, in order to
893: * verify the signature.
894: *
895: * @param string|null $cryptKeyPath The path to the PEM private key file
896: * that will be used to decrypt the session key which was used to
897: * encrypt the token. Required to analyze an encrypted token, optional
898: * to create one.
899: *
900: * @param string|null $cryptKeyPassword The password to access the private
901: * key that will be used to decrypt the session key which was used to
902: * encrypt the token. Use NULL if no password needed.
903: *
904: * @param string $cryptCertPath The path to the PEM X.509 certificate file
905: * that will be used to encrypt the session key which will be used to
906: * encrypt the token.
907: *
908: * @param array $options Configuration options.
909: *
910: * @throws Exception
911: *
912: * @see createXMLToken(), createSecureXMLToken(), analyzeXMLToken(),
913: * analyzeSecureXMLToken()
914: */
915: protected function __construct(
916: $xmlOrData,
917: $signKeyPath = null,
918: $signKeyPassword = null,
919: $signCertPath = null,
920: $cryptKeyPath = null,
921: $cryptKeyPassword = null,
922: $cryptCertPath = null,
923: $options = []
924: )
925: {
926: if (!is_array($xmlOrData) && !is_string($xmlOrData))
927: {
928: throw new Exception("Invalid parameter 'xmlOrData'! Parameter must be either an array or a string!");
929: }
930:
931: // Check the options against what the object is created for: an
932: // unknown option, or an option that does not apply here, throws
933: // (see _checkOptions()).
934: $isCreation = is_array($xmlOrData);
935: self::_checkOptions($options, $isCreation, !is_null($isCreation ? $cryptCertPath : $cryptKeyPath));
936:
937: // Load configuration options.
938: $base64Encode = isset($options['base64Encode']) ? $options['base64Encode'] : true;
939:
940: $xmldsigNsPrefix = isset($options['xmldsigNsPrefix']) ? $options['xmldsigNsPrefix'] : null;
941:
942: $checkSigningAlgorithms = isset($options['checkSigningAlgorithms']) ? $options['checkSigningAlgorithms'] : true;
943: $checkCryptingAlgorithms = isset($options['checkCryptingAlgorithms']) ? $options['checkCryptingAlgorithms'] : true;
944:
945: $canonicalizationAlgorithm = isset($options['canonicalizationAlgorithm']) ? $options['canonicalizationAlgorithm'] : null;
946: $signatureAlgorithm = isset($options['signatureAlgorithm']) ? $options['signatureAlgorithm'] : null;
947: $digestAlgorithm = isset($options['digestAlgorithm']) ? $options['digestAlgorithm'] : null;
948: $sessionKeyCipheringAlgorithm = isset($options['sessionKeyCipheringAlgorithm']) ? $options['sessionKeyCipheringAlgorithm'] : null;
949: $cryptAlgorithm = isset($options['cryptAlgorithm']) ? $options['cryptAlgorithm'] : null;
950:
951: // If provided, set and store the canonicalization algorithm.
952: if (!is_null($canonicalizationAlgorithm))
953: {
954: if (self::_isValidCanonicalizationAlgorithm($canonicalizationAlgorithm))
955: {
956: $this->canonicalizationAlgorithm = $canonicalizationAlgorithm;
957: $this->transformAlgorithm = $canonicalizationAlgorithm;
958: }
959: else
960: {
961: throw new Exception("Invalid parameter 'canonicalizationAlgorithm'! Unknown canonicalization algorithm.");
962: }
963: }
964:
965: // If provided, set and store the signature algorithm.
966: if (!is_null($signatureAlgorithm))
967: {
968: if (self::_isValidSignatureAlgorithm($signatureAlgorithm))
969: {
970: $this->signatureAlgorithm = $signatureAlgorithm;
971: }
972: else
973: {
974: throw new Exception("Invalid parameter 'signatureAlgorithm'! Unknown signature algorithm.");
975: }
976: }
977:
978: // If provided, set and store the digest algorithm.
979: if (!is_null($digestAlgorithm))
980: {
981: if (self::_isValidHashAlgorithm($digestAlgorithm))
982: {
983: $this->digestAlgorithm = $digestAlgorithm;
984: }
985: else
986: {
987: throw new Exception("Invalid parameter 'digestAlgorithm'! Unknown digest algorithm.");
988: }
989: }
990:
991: // If requested, disable check of signature algorithms.
992: if (false === $checkSigningAlgorithms)
993: {
994: $this->checkSigningAlgorithms = false;
995: }
996:
997: // Check what have to be done according to the type of $xmlOrData
998: // parameter.
999: if ($isCreation)
1000: {
1001: // $xmlOrData is an array, so we have to create an XML token.
1002:
1003: // Check that $data is a non-empty array.
1004: if (empty($xmlOrData))
1005: {
1006: throw new Exception("Invalid parameter 'data'! Token data should not be an empty array.");
1007: }
1008:
1009: // Check that $data is an associative array.
1010: if (count(array_filter(array_keys($xmlOrData), 'is_string')) == 0)
1011: {
1012: throw new Exception("Invalid parameter 'data'! Token data should be an associative array.");
1013: }
1014:
1015: // If requested, disable base64 encoding of token data.
1016: if (false === $base64Encode)
1017: {
1018: $this->base64EncodeData = false;
1019: }
1020:
1021: // If provided, store the audiences and the validity of the token
1022: // (checked by _checkOptions()).
1023: if (isset($options['audience']))
1024: {
1025: $this->audienceToWrite = (array) $options['audience'];
1026: }
1027: if (isset($options['validity']))
1028: {
1029: $this->validity = $options['validity'];
1030: }
1031:
1032: // Check that required private key and X.509 certificate
1033: // are provided, in order to sign the token.
1034: if (is_null($signKeyPath))
1035: {
1036: throw new Exception("Missing parameter 'signKeyPath'! A private key is required to sign token.");
1037: }
1038:
1039: // Check that required private key and X.509 certificate
1040: // are provided, in order to sign the token.
1041: if (is_null($signCertPath))
1042: {
1043: throw new Exception("Missing parameter 'signCertPath'! An X.509 certificate is required to sign token.");
1044: }
1045:
1046: // Get and store the private key that will be used to sign the token
1047: // data.
1048: $signKey = @file_get_contents($signKeyPath);
1049: if (false === $signKey)
1050: {
1051: throw new Exception("Invalid parameter 'signKeyPath'! Cannot read private key to sign token.");
1052: }
1053: $this->signKey = $signKey;
1054:
1055: // If provided, store the password to access the private key that
1056: // will be used to sign the token data.
1057: if (!is_null($signKeyPassword))
1058: {
1059: if (!is_string($signKeyPassword))
1060: {
1061: throw new Exception("Invalid parameter 'signKeyPassword'! Parameter must be a string.");
1062: }
1063: $this->signKeyPassword = $signKeyPassword;
1064: }
1065:
1066: // Get and store the X.509 certificate that will be included in the
1067: // XML token.
1068: $signCert = @file_get_contents($signCertPath);
1069: if (false === $signCert)
1070: {
1071: throw new Exception("Invalid parameter 'signCertPath'! Cannot read X.509 certificate to sign token.");
1072: }
1073: $this->signCert = $signCert;
1074:
1075: // The created token is analyzed to check it (see _writeXML()): it
1076: // must be signed with this very certificate.
1077: $this->pinnedSignerCert = $signCert;
1078:
1079: // If provided, store the prefix namespace to use in the XML digital
1080: // signature tags.
1081: if (!is_null($xmldsigNsPrefix))
1082: {
1083: if (!is_string($xmldsigNsPrefix))
1084: {
1085: throw new Exception("Invalid parameter 'xmldsigNsPrefix'! Parameter must be a string.");
1086: }
1087: $this->xmldsigNsPrefix = $xmldsigNsPrefix;
1088: }
1089:
1090: // If provided, get and store X.509 certificate that will be used
1091: // to encrypt the session key which will be used to encrypt the
1092: // token data.
1093: if (!is_null($cryptCertPath))
1094: {
1095: $cryptCert = @file_get_contents($cryptCertPath);
1096: if (false === $cryptCert)
1097: {
1098: throw new Exception("Invalid parameter 'cryptCertPath'! Cannot read X.509 certificate to encrypt token.");
1099: }
1100: $this->cryptCert = $cryptCert;
1101:
1102: // If provided, get and store the private key of the recipient,
1103: // which decrypts the session key: it is only used to check
1104: // the decrypted content of the created token. Without it, the
1105: // created token is checked up to its decryption (see
1106: // _readXML()), so that the sender needs no private key of the
1107: // recipient.
1108: if (!is_null($cryptKeyPath))
1109: {
1110: $cryptKey = @file_get_contents($cryptKeyPath);
1111: if (false === $cryptKey)
1112: {
1113: throw new Exception("Invalid parameter 'cryptKeyPath'! Cannot read private key to decrypt token.");
1114: }
1115: $this->cryptKey = $cryptKey;
1116: }
1117:
1118: // If provided, store the password to access the private key
1119: // that will be used to decrypt the session key. A password
1120: // without a key would be ignored: it throws instead.
1121: if (!is_null($cryptKeyPassword))
1122: {
1123: if (is_null($cryptKeyPath))
1124: {
1125: throw new Exception("Invalid parameter 'cryptKeyPassword'! It only applies with the parameter 'cryptKeyPath'.");
1126: }
1127: if (!is_string($cryptKeyPassword))
1128: {
1129: throw new Exception("Invalid parameter 'cryptKeyPassword'! Parameter must be a string.");
1130: }
1131: $this->cryptKeyPassword = $cryptKeyPassword;
1132: }
1133:
1134: // If provided, set and store the session key algorithm.
1135: if (!is_null($sessionKeyCipheringAlgorithm))
1136: {
1137: if (self::_isValidSymmetricCipheringAlgorithm($sessionKeyCipheringAlgorithm))
1138: {
1139: $this->sessionKeyCipheringAlgorithm = $sessionKeyCipheringAlgorithm;
1140: }
1141: else
1142: {
1143: throw new Exception("Invalid parameter 'sessionKeyCipheringAlgorithm'! Unknown session key ciphering algorithm.");
1144: }
1145: }
1146:
1147: // If provided, set and store the crypt algorithm.
1148: if (!is_null($cryptAlgorithm))
1149: {
1150: if (self::_isValidKeyTransportAlgorithm($cryptAlgorithm))
1151: {
1152: $this->cryptAlgorithm = $cryptAlgorithm;
1153: }
1154: else
1155: {
1156: throw new Exception("Invalid parameter 'cryptAlgorithm'! Unknown crypt algorithm.");
1157: }
1158: }
1159:
1160: // If requested, disable check of used crypt algorithms.
1161: if (false === $checkCryptingAlgorithms)
1162: {
1163: $this->checkCryptingAlgorithms = false;
1164: }
1165: }
1166:
1167: // Create an XML token with the provided data.
1168: try
1169: {
1170: // NOTE: This function will call the _readXML() function which
1171: // will verify that the created token is correct and which will
1172: // terminate setting object properties.
1173: $this->_writeXML($xmlOrData);
1174: }
1175: catch (Exception $e)
1176: {
1177: // ERROR: XML token analysis failed on fatal error.
1178: $this->error = $e->getMessage();
1179: }
1180: }
1181: else
1182: {
1183: // $xmlOrData is a string, so we have to analyze the XML token.
1184:
1185: // Load the trust anchor. Its absence is not an error here: the
1186: // analysis then reports that no certificate can be trusted (see
1187: // _verifyCertTrust()).
1188: $this->_loadTrustOptions($options);
1189:
1190: // If provided, store the audience the token must be addressed to
1191: // (checked by _checkOptions(), see _verifyAudience()).
1192: if (isset($options['expectedAudience']))
1193: {
1194: $this->expectedAudience = $options['expectedAudience'];
1195: }
1196:
1197: // If provided, get and store the private key that will be used
1198: // to decrypt the session key which was used to encrypt the token.
1199: if (!is_null($cryptKeyPath))
1200: {
1201: $cryptKey = @file_get_contents($cryptKeyPath);
1202: if (false === $cryptKey)
1203: {
1204: throw new Exception("Invalid parameter 'cryptKeyPath'! Cannot read private key to decrypt token.");
1205: }
1206: $this->cryptKey = $cryptKey;
1207:
1208: // If provided, store the password to access the private key
1209: // that will be used to decrypt the session key.
1210: if (!is_null($cryptKeyPassword))
1211: {
1212: if (!is_string($cryptKeyPassword))
1213: {
1214: throw new Exception("Invalid parameter 'cryptKeyPassword'! Parameter must be a string.");
1215: }
1216: $this->cryptKeyPassword = $cryptKeyPassword;
1217: }
1218:
1219: // If provided, set and store the session key algorithm.
1220: if (!is_null($sessionKeyCipheringAlgorithm))
1221: {
1222: if (self::_isValidSymmetricCipheringAlgorithm($sessionKeyCipheringAlgorithm))
1223: {
1224: $this->sessionKeyCipheringAlgorithm = $sessionKeyCipheringAlgorithm;
1225: }
1226: else
1227: {
1228: throw new Exception("Invalid parameter 'sessionKeyCipheringAlgorithm'! Unknown session key ciphering algorithm.");
1229: }
1230: }
1231:
1232: // If provided, set and store the crypt algorithm.
1233: if (!is_null($cryptAlgorithm))
1234: {
1235: if (self::_isValidKeyTransportAlgorithm($cryptAlgorithm))
1236: {
1237: $this->cryptAlgorithm = $cryptAlgorithm;
1238: }
1239: else
1240: {
1241: throw new Exception("Invalid parameter 'cryptAlgorithm'! Unknown crypt algorithm.");
1242: }
1243: }
1244:
1245: // If requested, disable check of used crypt algorithms.
1246: if (false === $checkCryptingAlgorithms)
1247: {
1248: $this->checkCryptingAlgorithms = false;
1249: }
1250: }
1251:
1252: // Parse the provided XML token to ensure all is correct and to
1253: // terminate initializing object properties.
1254: try
1255: {
1256: $this->_readXML($xmlOrData);
1257: }
1258: catch (Exception $e)
1259: {
1260: // ERROR: XML token analysis failed on fatal error.
1261: $this->error = $e->getMessage();
1262: }
1263: }
1264: }
1265:
1266:
1267: /**
1268: * Check the options given to the object against what it is created for.
1269: *
1270: * Each option is only read in its context (see `OPTION_CONTEXTS`): token
1271: * creation, token analysis, both ("any"), encryption (creation with an
1272: * encryption certificate, or analysis with a decryption key) or
1273: * decryption (analysis with a decryption key). A misspelt option, or an
1274: * option given out of its context, would be ignored without notice and
1275: * leave the token without the configuration the caller asked for: it
1276: * throws an exception instead, as does an option whose value is NULL or
1277: * of the wrong type. The values of the algorithm and trust options are
1278: * checked where they are loaded.
1279: *
1280: * @param array $options Configuration options.
1281: *
1282: * @param boolean $isCreation TRUE if the object creates a token, FALSE if
1283: * it analyzes one.
1284: *
1285: * @param boolean $isEncryption TRUE if an encryption certificate (token
1286: * creation) or a decryption key (token analysis) is given.
1287: *
1288: * @throws Exception
1289: */
1290: static private function _checkOptions($options, $isCreation, $isEncryption)
1291: {
1292: if (!is_array($options))
1293: {
1294: throw new Exception("Invalid parameter 'options'! Parameter must be an array.");
1295: }
1296:
1297: foreach ($options as $name => $value)
1298: {
1299: if (!isset(self::OPTION_CONTEXTS[$name]))
1300: {
1301: throw new Exception("Unknown option '" . $name . "'!");
1302: }
1303:
1304: switch (self::OPTION_CONTEXTS[$name])
1305: {
1306: case 'creation':
1307: if (!$isCreation)
1308: {
1309: throw new Exception("Option '" . $name . "' only applies to token creation!");
1310: }
1311: break;
1312: case 'analysis':
1313: if ($isCreation)
1314: {
1315: throw new Exception("Option '" . $name . "' only applies to token analysis! A created token is checked with the certificate of its own signing key.");
1316: }
1317: break;
1318: case 'encryption':
1319: if (!$isEncryption)
1320: {
1321: throw new Exception("Option '" . $name . "' only applies to an encrypted token! Give the encryption certificate (createSecureXMLToken()) or the decryption key (analyzeSecureXMLToken()).");
1322: }
1323: break;
1324: case 'decryption':
1325: if ($isCreation || !$isEncryption)
1326: {
1327: throw new Exception("Option '" . $name . "' only applies to the analysis of an encrypted token! Give the decryption key (analyzeSecureXMLToken()).");
1328: }
1329: break;
1330: }
1331:
1332: if (is_null($value))
1333: {
1334: throw new Exception("Invalid option '" . $name . "'! Its value must not be NULL.");
1335: }
1336:
1337: if (in_array($name, ['base64Encode', 'checkSigningAlgorithms', 'checkCryptingAlgorithms'], true) && !is_bool($value))
1338: {
1339: throw new Exception("Invalid option '" . $name . "'! It must be a boolean.");
1340: }
1341:
1342: if ('xmldsigNsPrefix' === $name && !is_string($value))
1343: {
1344: throw new Exception("Invalid option '" . $name . "'! It must be a string.");
1345: }
1346:
1347: if ('audience' === $name)
1348: {
1349: $audiences = is_array($value) ? $value : [$value];
1350: if (empty($audiences) || $audiences !== array_values($audiences) || $audiences !== array_filter($audiences, [self::class, '_isValidAudience']))
1351: {
1352: throw new Exception("Invalid option 'audience'! It must be a non-empty string, or a non-empty list of them, without control characters.");
1353: }
1354: if (count(array_unique($audiences)) !== count($audiences))
1355: {
1356: throw new Exception("Invalid option 'audience'! An audience is given twice.");
1357: }
1358: }
1359:
1360: if ('expectedAudience' === $name && !self::_isValidAudience($value))
1361: {
1362: throw new Exception("Invalid option 'expectedAudience'! It must be a non-empty string without control characters.");
1363: }
1364:
1365: if ('validity' === $name && !(is_int($value) && $value > 0))
1366: {
1367: throw new Exception("Invalid option 'validity'! It must be a number of seconds greater than 0.");
1368: }
1369:
1370: // The expiry is written with a four-digit year: a validity that
1371: // takes it past the year 9999 (or past the range of an integer)
1372: // could not be written, and would be computed wrong.
1373: if ('validity' === $name && $value > self::MAX_EXPIRY_TIME - time())
1374: {
1375: throw new Exception("Invalid option 'validity'! The expiry would be after 9999-12-31T23:59:59Z.");
1376: }
1377: }
1378: }
1379:
1380:
1381: /**
1382: * Indicates whether a value can be an audience of a token (options
1383: * "audience" and "expectedAudience"): a non-empty UTF-8 string, without
1384: * surrounding white space, made of characters XML 1.0 can hold as text.
1385: *
1386: * Control characters are refused: XML 1.0 cannot hold most of them, and
1387: * the others (tab, line feed, carriage return) are normalized by XML
1388: * parsers, so that the audience read back would differ from the one
1389: * written. So are U+FFFE and U+FFFF, which XML 1.0 cannot hold either.
1390: *
1391: * @param mixed $audience The value to check.
1392: *
1393: * @return boolean TRUE if the value can be an audience, FALSE otherwise.
1394: */
1395: static private function _isValidAudience($audience)
1396: {
1397: return is_string($audience)
1398: && '' !== $audience
1399: && trim($audience) === $audience
1400: && 1 === preg_match('//u', $audience)
1401: && 0 === preg_match('/[\x00-\x1F\x7F]|[^\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', $audience);
1402: }
1403:
1404:
1405: /**
1406: * Load and check the trust options of an analysis ("caCertPath",
1407: * "signerCertPath" and "expectedSubject", see `__construct()`).
1408: *
1409: * A trust option of the wrong type, or naming a file that cannot be read,
1410: * throws an exception instead of being ignored: ignoring it would leave
1411: * the token without the constraint the caller asked for.
1412: *
1413: * @param array $options Configuration options.
1414: *
1415: * @throws Exception
1416: */
1417: private function _loadTrustOptions($options)
1418: {
1419: if (isset($options['caCertPath']))
1420: {
1421: $caCertPaths = $options['caCertPath'];
1422: if (is_string($caCertPaths))
1423: {
1424: $caCertPaths = [$caCertPaths];
1425: }
1426: if (!is_array($caCertPaths) || empty($caCertPaths))
1427: {
1428: throw new Exception("Invalid option 'caCertPath'! It must be a path or a non-empty list of paths.");
1429: }
1430: foreach ($caCertPaths as $caCertPath)
1431: {
1432: if (!is_string($caCertPath) || !is_file($caCertPath) || !is_readable($caCertPath))
1433: {
1434: throw new Exception("Invalid option 'caCertPath'! Cannot read CA certificate file: " . (is_string($caCertPath) ? $caCertPath : gettype($caCertPath)) . ".");
1435: }
1436: }
1437: $this->trustedCACertPaths = array_values($caCertPaths);
1438: }
1439:
1440: if (isset($options['signerCertPath']))
1441: {
1442: $signerCert = is_string($options['signerCertPath']) ? @file_get_contents($options['signerCertPath']) : false;
1443: if (false === $signerCert || false === @openssl_x509_read($signerCert))
1444: {
1445: throw new Exception("Invalid option 'signerCertPath'! Cannot read the pinned X.509 certificate.");
1446: }
1447: $this->pinnedSignerCert = $signerCert;
1448: }
1449:
1450: if (isset($options['expectedSubject']))
1451: {
1452: $expectedSubject = $options['expectedSubject'];
1453: if (!(is_string($expectedSubject) || is_array($expectedSubject)) || empty($expectedSubject))
1454: {
1455: throw new Exception("Invalid option 'expectedSubject'! It must be a non-empty string or array.");
1456: }
1457: $this->expectedSubject = $expectedSubject;
1458: }
1459: }
1460:
1461:
1462: /**
1463: * Creates an XML token for the given user data.
1464: *
1465: * The created XML token can be retrieved using the function `getXML()`.
1466: *
1467: * @param array $data Token data (an associative array that may be
1468: * multi-dimensional).
1469: *
1470: * @param string $signKeyPath The path to the PEM private key file that will
1471: * be used to sign the token.
1472: *
1473: * @param string $signCertPath The path to the PEM X.509 certificate file
1474: * that will be included in the XML digital signature, in order to
1475: * verify the signature.
1476: *
1477: * @param string $signKeyPassword The password to access the private key
1478: * that will be used to sign the token. Use NULL if no password needed.
1479: *
1480: * @param array $options Configuration options
1481: * (see `__construct()` for details).
1482: *
1483: * @return XMLDSigToken An XMLDSigToken object. If the token cannot be
1484: * created, `getError()` tells why.
1485: *
1486: * @throws Exception
1487: *
1488: * @see __construct()
1489: */
1490: public static function createXMLToken(
1491: $data,
1492: $signKeyPath,
1493: $signCertPath,
1494: $signKeyPassword = null,
1495: $options = []
1496: )
1497: {
1498: return new XMLDSigToken(
1499: $data,
1500: $signKeyPath,
1501: $signKeyPassword,
1502: $signCertPath,
1503: $cryptKeyPath = null,
1504: $cryptKeyPassword = null,
1505: $cryptCertPath = null,
1506: $options
1507: );
1508: }
1509:
1510:
1511: /**
1512: * Creates a secure (encrypted) XML token for the given user data.
1513: *
1514: * The created XML token can be retrieved using the function `getXML()`.
1515: *
1516: * The token is encrypted for the owner of the encryption certificate
1517: * (`$cryptCertPath`): only the private key of this certificate decrypts
1518: * it. The sender does not need this private key: pass NULL as
1519: * `$cryptKeyPath`.
1520: *
1521: * ```php
1522: * $token = XMLDSigToken::createSecureXMLToken($data, $signKeyPath, $signCertPath, null, $cryptCertPath, $signKeyPassword);
1523: * ```
1524: *
1525: * The created token is checked right away:
1526: *
1527: * - Without `$cryptKeyPath`, the check covers everything that can be
1528: * verified without decrypting the token: its structure, its
1529: * algorithms, its digest, its signature and the trust in the signing
1530: * certificate. `isSignatureValid()` and `isDataEncrypted()` return
1531: * TRUE, while `getData()`, `getTimestamp()` and `getTokenId()` return
1532: * NULL: they are inside the encrypted content, which is not decrypted.
1533: *
1534: * - With `$cryptKeyPath`, the token is also decrypted and its content
1535: * read, as by `analyzeSecureXMLToken()`: `getData()`, `getTimestamp()`
1536: * and `getTokenId()` return what the decrypted token holds.
1537: *
1538: * @param array $data Token data (an associative array that may be
1539: * multi-dimensional).
1540: *
1541: * @param string $signKeyPath The path to the PEM private key file that will
1542: * be used to sign the token.
1543: *
1544: * @param string $signCertPath The path to the PEM X.509 certificate file
1545: * that will be included in the XML digital signature, in order to
1546: * verify the signature.
1547: *
1548: * @param string|null $cryptKeyPath The path to the PEM private key file of
1549: * the recipient, associated to `$cryptCertPath`, used to decrypt the
1550: * created token in order to check its content. Use NULL to create the
1551: * token without it.
1552: *
1553: * @param string $cryptCertPath The path to the PEM X.509 certificate file
1554: * that will be used to encrypt the session key which will be used to
1555: * encrypt the token.
1556: *
1557: * @param string $signKeyPassword The password to access the private key
1558: * that will be used to sign the token. Use NULL if no password needed.
1559: *
1560: * @param string|null $cryptKeyPassword The password to access the private
1561: * key `$cryptKeyPath`. Use NULL if no password needed, and when
1562: * `$cryptKeyPath` is NULL.
1563: *
1564: * @param array $options Configuration options
1565: * (see `__construct()` for details).
1566: *
1567: * @return XMLDSigToken An XMLDSigToken object. If the token cannot be
1568: * created, `getError()` tells why.
1569: *
1570: * @throws Exception
1571: *
1572: * @see __construct()
1573: */
1574: public static function createSecureXMLToken(
1575: $data,
1576: $signKeyPath,
1577: $signCertPath,
1578: $cryptKeyPath,
1579: $cryptCertPath,
1580: $signKeyPassword = null,
1581: $cryptKeyPassword = null,
1582: $options = []
1583: )
1584: {
1585: // Without an encryption certificate, the constructor would create an
1586: // unencrypted token.
1587: if (is_null($cryptCertPath))
1588: {
1589: throw new Exception("Missing parameter 'cryptCertPath'! An X.509 certificate is required to encrypt token.");
1590: }
1591:
1592: return new XMLDSigToken(
1593: $data,
1594: $signKeyPath,
1595: $signKeyPassword,
1596: $signCertPath,
1597: $cryptKeyPath,
1598: $cryptKeyPassword,
1599: $cryptCertPath,
1600: $options
1601: );
1602: }
1603:
1604:
1605: /**
1606: * Parse an XML token.
1607: *
1608: * The options must hold a trust anchor, `caCertPath` and/or
1609: * `signerCertPath`; without it, `isSignatureValid()` returns FALSE.
1610: *
1611: * ```php
1612: * $token = XMLDSigToken::analyzeXMLToken($xml, [
1613: * 'caCertPath' => ['path/to/intermediate.cert.pem', 'path/to/root.cert.pem'],
1614: * ]);
1615: * ```
1616: *
1617: * @param string $xml An XML token.
1618: *
1619: * @param array $options Configuration options, trust anchor included
1620: * (see `__construct()` for details).
1621: *
1622: * @return XMLDSigToken An XMLDSigToken object. If the token is not
1623: * valid, `isSignatureValid()` returns FALSE and `getError()` tells
1624: * why.
1625: *
1626: * @throws Exception
1627: *
1628: * @see __construct()
1629: */
1630: public static function analyzeXMLToken(
1631: $xml,
1632: $options = []
1633: )
1634: {
1635: return new XMLDSigToken(
1636: $xml,
1637: $signKeyPath = null,
1638: $signKeyPassword = null,
1639: $signCertPath = null,
1640: $cryptKeyPath = null,
1641: $cryptKeyPassword = null,
1642: $cryptCertPath = null,
1643: $options
1644: );
1645: }
1646:
1647:
1648: /**
1649: * Parse an XML token whose data are encrypted.
1650: *
1651: * NOTE: An unencrypted token can also be parsed using this function.
1652: *
1653: * The options must hold a trust anchor, `caCertPath` and/or
1654: * `signerCertPath`; without it, `isSignatureValid()` returns FALSE.
1655: *
1656: * @param string $xml An XML token.
1657: *
1658: * @param string $cryptKeyPath The path to the PEM private key file that
1659: * will be used to decrypt the session key which was used to encrypt
1660: * token data.
1661: *
1662: * @param string $cryptKeyPassword The password to access the private key
1663: * that will be used to decrypt the session key which was used to
1664: * encrypt the token. Use NULL if no password needed.
1665: *
1666: * @param array $options Configuration options, trust anchor included
1667: * (see `__construct()` for details).
1668: *
1669: * @return XMLDSigToken An XMLDSigToken object. If the token is not
1670: * valid, `isSignatureValid()` returns FALSE and `getError()` tells
1671: * why.
1672: *
1673: * @throws Exception
1674: *
1675: * @see __construct()
1676: */
1677: public static function analyzeSecureXMLToken(
1678: $xml,
1679: $cryptKeyPath,
1680: $cryptKeyPassword = null,
1681: $options = []
1682: )
1683: {
1684: return new XMLDSigToken(
1685: $xml,
1686: $signKeyPath = null,
1687: $signKeyPassword = null,
1688: $signCertPath = null,
1689: $cryptKeyPath,
1690: $cryptKeyPassword,
1691: $cryptCertPath = null,
1692: $options
1693: );
1694: }
1695:
1696:
1697: /**
1698: * Write the XML Digital Signature representing the XML token for the
1699: * provided user data.
1700: *
1701: * The XML token can then be obtained using the `getXML()` function.
1702: *
1703: * @param array $data A flat or multidimensional associative array
1704: * containing the token data.
1705: *
1706: * @throws Exception
1707: */
1708: private function _writeXML($data)
1709: {
1710: // Whether to base64 encode data or not.
1711: $base64Encode = $this->base64EncodeData;
1712:
1713: // Create an XML document to hold the token data.
1714: $xmlDoc = new DOMDocument('1.0', 'UTF-8');
1715:
1716: // Do not format the XML digital signature when it will be saved as an
1717: // XML string.
1718: $xmlDoc->formatOutput = false;
1719:
1720: // Create the root node "Token" that holds the whole content of the
1721: // token.
1722: // NOTE: This node is explicitly created without namespace if no
1723: // namespace is used for XML digital signature tags.
1724: if ($this->xmldsigNsPrefix === '')
1725: {
1726: $tokenElement = $xmlDoc->createElementNS('', self::TOKEN_NAME);
1727: }
1728: else
1729: {
1730: $tokenElement = $xmlDoc->createElement(self::TOKEN_NAME);
1731: }
1732: $tokenNode = $xmlDoc->appendChild($tokenElement);
1733:
1734: // Create a node "timestamp" that holds the token timestamp in the
1735: // format "YYYY-MM-DDThh:mm:ssZ", using UTC timezone, and append it to
1736: // the node "Token".
1737: $dt = new DateTime();
1738: $dt->setTimeZone(new DateTimeZone('UTC'));
1739: $tokenTimestampElement = $xmlDoc->createElement('TokenTimestamp', $dt->format(self::TOKEN_TIMESTAMP_FORMAT));
1740: $tokenTimestampNode = $tokenNode->appendChild($tokenTimestampElement);
1741:
1742: // Create a node "TokenId" that holds a random identifier, in
1743: // hexadecimal, and append it to the node "Token". The signature
1744: // covers it: the receiver can refuse a token whose identifier it has
1745: // already accepted (see getTokenId()).
1746: $tokenIdElement = $xmlDoc->createElement(self::TOKEN_ID_NAME, bin2hex(random_bytes(self::TOKEN_ID_BYTES)));
1747: $tokenNode->appendChild($tokenIdElement);
1748:
1749: // If a validity is given, create a node "TokenExpiry" that holds the
1750: // date after which the token is out of date (see isOutOfDate()), in
1751: // the format of the timestamp, and append it to the node "Token".
1752: if (!is_null($this->validity))
1753: {
1754: $expiry = clone $dt;
1755: $expiry->modify('+' . $this->validity . ' seconds');
1756: $tokenNode->appendChild($xmlDoc->createElement(self::TOKEN_EXPIRY_NAME, $expiry->format(self::TOKEN_TIMESTAMP_FORMAT)));
1757: }
1758:
1759: // Create a node "TokenAudience" per audience the token is addressed to
1760: // (see getAudience()), and append them to the node "Token". The value
1761: // is added as a text node: the second argument of createElement()
1762: // would read "&" as the start of an entity reference.
1763: foreach ((array) $this->audienceToWrite as $audience)
1764: {
1765: $audienceElement = $xmlDoc->createElement(self::TOKEN_AUDIENCE_NAME);
1766: $audienceElement->appendChild($xmlDoc->createTextNode($audience));
1767: $tokenNode->appendChild($audienceElement);
1768: }
1769:
1770: /**
1771: * Inline function to build a node tree, whose root node will be named
1772: * $nodeName, from the array $data and append it to the given
1773: * $parentNode node. The values of the leaves are base64 encoded by this
1774: * function.
1775: *
1776: * NOTE: This function is called recursively if $data is a
1777: * multi-dimensional associative array.
1778: *
1779: * @param array $data A flat or multi-dimensional associative array that
1780: * holds the data to convert in a node tree.
1781: *
1782: * @param string $nodeName The name of the node that will hold the node
1783: * tree.
1784: *
1785: * @param DOMNode $parentNode An existing DOM node to which to append
1786: * the node tree.
1787: */
1788: $dataToNodeTree = function($data, $nodeName, $parentNode) use (&$dataToNodeTree, $base64Encode)
1789: {
1790: if(is_array($data))
1791: {
1792: // Appends node.
1793: $element = $parentNode->ownerDocument->createElement($nodeName);
1794: $dataNode = $parentNode->appendChild($element);
1795:
1796: // Append child nodes/leaves recursively.
1797: array_walk($data, $dataToNodeTree, $dataNode);
1798: }
1799: else
1800: {
1801: // Appends leaf.
1802: $element = $parentNode->ownerDocument->createElement($nodeName, $base64Encode ? base64_encode($data) : $data);
1803: if ($base64Encode)
1804: {
1805: $attribute = $parentNode->ownerDocument->createAttribute('Algorithm');
1806: $attribute->value = 'base64';
1807: $element->appendChild($attribute);
1808: }
1809: $dataNode = $parentNode->appendChild($element);
1810: }
1811: };
1812:
1813: // Create a node "TokenData" that holds a node tree representing the
1814: // token data encoded in base64 and append it to the node "Token".
1815: $dataToNodeTree($data, self::TOKEN_DATA_NAME, $tokenNode);
1816:
1817: // Create an XMLSecurityDSig object.
1818: // This object will hold the XML Digital Signature we are building.
1819: $XMLDSig = new XMLSecurityDSig($this->xmldsigNsPrefix);
1820: $XMLDSig->setCanonicalMethod($this->canonicalizationAlgorithm);
1821:
1822: // Add a node "ds:Object" to the XML Digital Signature and attach our
1823: // node "Token" to it.
1824: $objectNode = $XMLDSig->addObject($xmlDoc->documentElement);
1825: $objectNode->setAttribute('Id', self::TOKEN_NAME);
1826:
1827: // Encrypt our node "Token" if requested (if an X.509 certificate is
1828: // provided to perform encryption).
1829: if (!is_null($this->cryptCert))
1830: {
1831: // Randomly generate a session key for the symmetric algorithm.
1832: // This key will be used to cipher our node "Token" (the content of
1833: // the node "ds:Object").
1834: $sessionKey = new XMLSecurityKey($this->sessionKeyCipheringAlgorithm);
1835: $sessionKey->generateSessionKey();
1836:
1837: // Create an asymmetric key from the X.509 certificate using RSA-OAEP
1838: // algorithm. This key will be used to encrypt the session key.
1839: $asymKey = new XMLSecurityKey($this->cryptAlgorithm, array('type' => 'public'));
1840: $asymKey->loadKey($this->cryptCert, false, true);
1841: if (empty($asymKey->key))
1842: {
1843: // ERROR: Failed loading certificate to encrypt session key!
1844: throw new Exception("Failed loading certificate to encrypt session key!");
1845: }
1846:
1847: // Encrypt the session key using the asymmetric key.
1848: $objXMLSecEnc = new XMLSecEnc();
1849: $objXMLSecEnc->encryptKey($asymKey, $sessionKey);
1850:
1851: // Encrypt the content (cf. XMLSecEnc::Content) of the node
1852: // "ds:Object" that holds our node "Token" using the encrypted
1853: // session key.
1854: $objXMLSecEnc->setNode($objectNode);
1855: $objXMLSecEnc->type = XMLSecEnc::Content;
1856: $encryptedObjectNode = $objXMLSecEnc->encryptNode($sessionKey);
1857: }
1858:
1859: // Add a reference to the node "ds:Object" which is contained in the
1860: // XML Digital Signature itself (case of an enveloping signature).
1861: // This consists of:
1862: // - Adding a node "ds:Reference" to the node "ds:SignedInfo".
1863: // - Computing a hash, using the digest algorithm, from the
1864: // canonicalized XML string corresponding to the node "ds:Object".
1865: // - Adding this hash in a child node named "ds:DigestValue".
1866: //
1867: // NOTE: Only the first child node of the node "ds:Object" is taken in
1868: // account to generate the hash which will then be signed.
1869: $XMLDSig->addReference($objectNode, $this->digestAlgorithm, NULL, array('overwrite' => false, 'force_uri' => true));
1870:
1871: // Create the private key object that will be used to sign the token
1872: // applying the signature algorithm. If a password is provided, use it
1873: // to access the private key.
1874: $privateKey = new XMLSecurityKey($this->signatureAlgorithm, array('type' => 'private'));
1875: if (!is_null($this->signKeyPassword))
1876: {
1877: $privateKey->passphrase = $this->signKeyPassword;
1878: }
1879:
1880: $privateKey->loadKey($this->signKey, false, false);
1881: if (empty($privateKey->key))
1882: {
1883: // ERROR: Failed loading private key to sign token data!
1884: throw new Exception("Failed loading private key to sign token data!");
1885: }
1886:
1887: // Add the X.509 certificate associated to the private key to the XML
1888: // Digital Signature. So, the recipient of the XML token will not need
1889: // to own it, in order to verify the signature. It will simply get it
1890: // from the XML token. This consists of adding a node "ds:KeyInfo" that
1891: // holds the content of the X.509 certificate.
1892: $XMLDSig->add509Cert($this->signCert);
1893:
1894: // Sign the token data using the private security key associated to the
1895: // X.509 certificate.
1896: // This consists of:
1897: // - Computing the signature of the node "ds:SignedInfo".
1898: // - Adding a node named "ds:SignatureValue" that contains the
1899: // computed signature.
1900: $XMLDSig->sign($privateKey);
1901:
1902: // Check that the created XML token is well formed (if it's valid and if
1903: // timestamp and data can be retrieved from it) and, by the way,
1904: // terminate to initialize the properties of our XMLDSigToken object.
1905: $this->_readXML($XMLDSig->sigNode->ownerDocument->saveXML(), true);
1906: }
1907:
1908:
1909: /**
1910: * Read the XML Digital Signature representing an XML token.
1911: *
1912: * This function will check that the provided XML Digital Signature is
1913: * valid and that its certificate is trusted (see `_verifyCertTrust()`),
1914: * will decrypt the token data it contains if this one is encrypted
1915: * and then will extract timestamp and data.
1916: *
1917: * The data can then be obtained using the `getData()` function. Nevertheless,
1918: * you have to ensure that you can trust it using `isSignatureValid()` and
1919: * `isOutOfDate()`.
1920: *
1921: * NOTE: This function is called by the `_writeXML()` function to ensure the
1922: * XML token it built is correct and because this function sets most of the
1923: * properties of the XMLDSigToken object that are issued from the analysis
1924: * of the XML token. A token created encrypted without the private key of
1925: * the recipient is checked up to its decryption: structure, algorithms,
1926: * digest, signature and trust in the signing certificate. Its content is
1927: * not decrypted, and neither its timestamp, its identifier nor its data
1928: * are read.
1929: *
1930: * @param string $xml The XML Digital Signature representing an XML token.
1931: *
1932: * @param boolean $isCreation TRUE if the object has just created the
1933: * token.
1934: *
1935: * @throws Exception
1936: */
1937: private function _readXML($xml, $isCreation = false)
1938: {
1939: // Store the provided XML Digital Signature.
1940: $this->xml = $xml;
1941:
1942: // Load the enveloping XML Digital Signature into a DOM document,
1943: // preserving its formatting. A document with a document type
1944: // declaration is refused before it is parsed (see _loadXML()).
1945: $xmlDoc = self::_loadXML($xml, "XML Digital Signature");
1946:
1947: // Instantiate and store a DOMXpath object to navigate through the DOM
1948: // document that holds the XML Digital Signature.
1949: $this->xpath = new DOMXPath($xmlDoc);
1950: $this->xpath->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
1951: $this->xpath->registerNamespace('xenc', XMLSecEnc::XMLENCNS);
1952:
1953: // xmlseclibs computes the digest over the first element of the
1954: // document whose attribute "Id" is "Token" and, when "ds:Signature" is
1955: // not the document element, it first detaches "ds:Signature" from the
1956: // document. A second element "Id=Token", or a "ds:Signature" wrapped
1957: // in another element, thus lets a copy of the signed "ds:Object",
1958: // placed where the signature does not cover it, be digested instead of
1959: // the "ds:Object" the token is read from (signature wrapping). Such
1960: // documents are rejected. The token is also read only from the node
1961: // xmlseclibs digested (see below).
1962: $root = $xmlDoc->documentElement;
1963: if (XMLSecurityDSig::XMLDSIGNS !== $root->namespaceURI || 'Signature' !== $root->localName)
1964: {
1965: // ERROR: XML Digital Signature is malformed!
1966: throw new Exception("XML digital signature is malformed! The document element must be ds:Signature.");
1967: }
1968: if ($this->xpath->query('//@*[local-name()="Id" and .="' . self::TOKEN_NAME . '"]')->length !== 1)
1969: {
1970: // ERROR: XML Digital Signature is malformed!
1971: throw new Exception("XML digital signature is malformed! Exactly one element must have the attribute Id=\"" . self::TOKEN_NAME . "\".");
1972: }
1973:
1974: // Verify that the XML Digital Signature is well formed.
1975: $queries = array(
1976: "//ds:Signature",
1977: "//ds:Signature/ds:SignedInfo",
1978: "//ds:Signature/ds:SignedInfo/ds:CanonicalizationMethod",
1979: "//ds:Signature/ds:SignedInfo/ds:SignatureMethod",
1980: "//ds:Signature/ds:SignedInfo/ds:Reference",
1981: "//ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms",
1982: "//ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform",
1983: "//ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestMethod",
1984: "//ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue",
1985: "//ds:Signature/ds:SignatureValue",
1986: "//ds:Signature/ds:KeyInfo",
1987: "//ds:Signature/ds:KeyInfo/ds:X509Data",
1988: "//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
1989: "//ds:Signature/ds:Object"
1990: );
1991:
1992: foreach ($queries as $query)
1993: {
1994: if ($this->xpath->query($query)->length !== 1)
1995: {
1996: // ERROR: XML Digital Signature is malformed!
1997: throw new Exception("XML digital signature is malformed!");
1998: }
1999: }
2000:
2001: $queries = array(
2002: // "//ds:Signature/@xmlns" => XMLSecurityDSig::XMLDSIGNS,
2003: "//ds:Signature/ds:SignedInfo/ds:Reference/@URI" => '#' . self::TOKEN_NAME,
2004: "//ds:Signature/ds:Object/@Id" => self::TOKEN_NAME
2005: );
2006:
2007: foreach ($queries as $query => $value)
2008: {
2009: if ($this->xpath->evaluate("string(" . $query . ")") !== $value)
2010: {
2011: // ERROR: XML digital signature is malformed!
2012: throw new Exception("XML digital signature is malformed!");
2013: }
2014: }
2015:
2016: // Verify the algorithms used to sign the token.
2017: $queries = array(
2018: "//ds:Signature/ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm"
2019: => array(
2020: 'name' => 'Canonicalization',
2021: 'validateFunction' => '_isValidCanonicalizationAlgorithm',
2022: 'expectedValue' => $this->canonicalizationAlgorithm
2023: ),
2024: "//ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm"
2025: => array(
2026: 'name' => 'Signature',
2027: 'validateFunction' => '_isValidSignatureAlgorithm',
2028: 'expectedValue' => $this->signatureAlgorithm
2029: ),
2030: "//ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform/@Algorithm"
2031: => array(
2032: 'name' => 'Transform',
2033: 'validateFunction' => '_isValidCanonicalizationAlgorithm',
2034: 'expectedValue' => $this->transformAlgorithm
2035: ),
2036: "//ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm"
2037: => array(
2038: 'name' => 'Digest',
2039: 'validateFunction' => '_isValidHashAlgorithm',
2040: 'expectedValue' => $this->digestAlgorithm
2041: )
2042: );
2043:
2044: foreach ($queries as $query => $algorithm)
2045: {
2046: $usedAlgorithm = $this->xpath->evaluate("string(" . $query . ")");
2047:
2048: if (empty($usedAlgorithm))
2049: {
2050: // ERROR: Algorithm is missing!
2051: throw new Exception($algorithm['name'] . " algorithm is missing!");
2052: }
2053:
2054: if (!call_user_func(self::class . '::' . $algorithm['validateFunction'], $usedAlgorithm))
2055: {
2056: // ERROR: Algorithm is invalid!
2057: throw new Exception($algorithm['name'] . " algorithm is invalid!");
2058: }
2059:
2060: if ($usedAlgorithm !== $algorithm['expectedValue'] && $this->checkSigningAlgorithms)
2061: {
2062: // ANOMALY: Unauthorized algorithm!
2063: $this->anomalies[] = "Unauthorized " . $algorithm['name'] . " algorithm! Expected: " . $algorithm['expectedValue'] . ", Used: " . $usedAlgorithm . ".";
2064: }
2065: }
2066:
2067: // Check whether the token is encrypted or not.
2068: if ($this->xpath->query("//ds:Signature/ds:Object/xenc:EncryptedData")->length === 1)
2069: {
2070: // Verify that the encrypted data is well formed.
2071: $queries = array(
2072: "//ds:Signature/ds:Object/xenc:EncryptedData/xenc:EncryptionMethod",
2073: "//ds:Signature/ds:Object/xenc:EncryptedData/ds:KeyInfo",
2074: "//ds:Signature/ds:Object/xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey",
2075: "//ds:Signature/ds:Object/xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod",
2076: "//ds:Signature/ds:Object/xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData",
2077: "//ds:Signature/ds:Object/xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue",
2078: "//ds:Signature/ds:Object/xenc:EncryptedData/xenc:CipherData",
2079: "//ds:Signature/ds:Object/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue"
2080: );
2081:
2082: foreach ($queries as $query)
2083: {
2084: if ($this->xpath->query($query)->length !== 1)
2085: {
2086: // ERROR: Encrypted data is malformed!
2087: throw new Exception("Encrypted data is malformed!");
2088: }
2089: }
2090:
2091: $queries = array(
2092: "//ds:Signature/ds:Object/xenc:EncryptedData/@Type" => XMLSecEnc::Content
2093: );
2094:
2095: foreach ($queries as $query => $value)
2096: {
2097: if ($this->xpath->evaluate("string(" . $query . ")") !== $value)
2098: {
2099: // ERROR: Encrypted data is malformed!
2100: throw new Exception("Encrypted data is malformed!");
2101: }
2102: }
2103:
2104: // Verify the algorithms used to encrypt the token.
2105: $queries = array(
2106: "//ds:Signature/ds:Object/xenc:EncryptedData/xenc:EncryptionMethod/@Algorithm"
2107: => array(
2108: 'name' => 'Session Key Ciphering',
2109: 'validateFunction' => '_isValidSymmetricCipheringAlgorithm',
2110: 'expectedValue' => $this->sessionKeyCipheringAlgorithm
2111: ),
2112: "//ds:Signature/ds:Object/xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod/@Algorithm"
2113: => array(
2114: 'name' => 'Crypt',
2115: 'validateFunction' => '_isValidKeyTransportAlgorithm',
2116: 'expectedValue' => $this->cryptAlgorithm
2117: )
2118: );
2119:
2120: foreach ($queries as $query => $algorithm)
2121: {
2122: $usedAlgorithm = $this->xpath->evaluate("string(" . $query . ")");
2123:
2124: if (empty($usedAlgorithm))
2125: {
2126: // ERROR: Algorithm is missing!
2127: throw new Exception($algorithm['name'] . " algorithm is missing!");
2128: }
2129:
2130: if (!call_user_func(self::class . '::' . $algorithm['validateFunction'], $usedAlgorithm))
2131: {
2132: // ERROR: Algorithm is invalid!
2133: throw new Exception($algorithm['name'] . " algorithm is invalid!");
2134: }
2135:
2136: if ($usedAlgorithm !== $algorithm['expectedValue'] && $this->checkCryptingAlgorithms)
2137: {
2138: // ANOMALY: Unauthorized algorithm!
2139: $this->anomalies[] = "Unauthorized " . $algorithm['name'] . " algorithm! Expected: " . $algorithm['expectedValue'] . ", Used: " . $usedAlgorithm . ".";
2140: }
2141: }
2142:
2143: // An algorithm without integrity (CBC) is only accepted when it
2144: // is the declared one, even if the check of the algorithms is
2145: // turned off (see _isAuthenticatedCipheringAlgorithm()).
2146: $usedAlgorithm = $this->xpath->evaluate("string(//ds:Signature/ds:Object/xenc:EncryptedData/xenc:EncryptionMethod/@Algorithm)");
2147: if (!self::_isAuthenticatedCipheringAlgorithm($usedAlgorithm)
2148: && $usedAlgorithm !== $this->sessionKeyCipheringAlgorithm
2149: && !$this->checkCryptingAlgorithms)
2150: {
2151: // ANOMALY: Undeclared unauthenticated algorithm!
2152: $this->anomalies[] = "Unauthorized Session Key Ciphering algorithm! " . $usedAlgorithm . " has no integrity: declare it with the option 'sessionKeyCipheringAlgorithm' to accept it.";
2153: }
2154: }
2155:
2156: // Create an XMLSecurityDSig object that rejects empty or failed
2157: // canonicalizations (see _createVerifier()).
2158: $XMLDSig = self::_createVerifier();
2159:
2160: // Locate the node "ds:Signature".
2161: $nodeSignature = $XMLDSig->locateSignature($xmlDoc);
2162: if (is_null($nodeSignature))
2163: {
2164: // ERROR: Cannot locate signature!
2165: throw new Exception("Cannot locate signature!");
2166: }
2167:
2168: // Build an XMLSecurityKey object from the X.509 certificate which is
2169: // included in the XML Digital Signature.
2170: $objKeyX509 = $XMLDSig->locateKey();
2171: if (!$objKeyX509)
2172: {
2173: // ERROR: Cannot locate X.509 certificate!
2174: throw new Exception("Cannot locate X.509 certificate!");
2175: }
2176:
2177: // Configure the $objKeyX509 XMLSecurityKey object with its related
2178: // information which should be stored in the node
2179: // "ds:Signature/ds:KeyInfo".
2180: if (is_null(XMLSecEnc::staticLocateKeyInfo($objKeyX509, $nodeSignature)))
2181: {
2182: // ERROR: Cannot locate key information for X.509 certificate!
2183: throw new Exception("Cannot locate key information for X.509 certificate!");
2184: }
2185:
2186: // Create a new X509Cert object for the X.509 certificate found in the
2187: // XML Digital Signature.
2188: $this->x509Certificate = new X509Cert($objKeyX509->getX509Certificate());
2189:
2190: // Verify that the hash computed from the node "ds:Signature/ds:Object"
2191: // is the same as the one stored in the node
2192: // "ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue", and get
2193: // the node the hash was computed over. The token is read from this
2194: // node only, never by searching the document: data, timestamp and
2195: // encrypted content are then those the digest covers.
2196: $nodeObject = null;
2197: try
2198: {
2199: $XMLDSig->validateReference();
2200:
2201: $validatedNodes = $XMLDSig->getValidatedNodes();
2202: if (isset($validatedNodes[self::TOKEN_NAME]))
2203: {
2204: $nodeObject = $validatedNodes[self::TOKEN_NAME];
2205: }
2206: if (is_null($nodeObject) || !$nodeObject->isSameNode($this->xpath->query("/ds:Signature/ds:Object")->item(0)))
2207: {
2208: throw new Exception("The digested node is not the node ds:Signature/ds:Object!");
2209: }
2210:
2211: $this->isDigestValueOk = true;
2212: }
2213: catch (Exception $e)
2214: {
2215: // ANOMALY: Token hash verification failed!
2216: $nodeObject = null;
2217: $this->isDigestValueOk = false;
2218: $this->anomalies[] = "Token hash verification failed!";
2219: }
2220:
2221: // Canonicalize the node "ds:Signature/ds:SignedInfo". The signature is
2222: // verified against this canonical form; the verifier throws an
2223: // exception if it is empty (see _createVerifier()).
2224: $XMLDSig->canonicalizeSignedInfo();
2225:
2226: // Verifies that the signature held in the node
2227: // "ds:Signature/ds:SignatureValue" is correct for the node
2228: // "ds:Signature/ds:SignedInfo" using the key we've just built from the
2229: // X.509 certificate found in the XML Digital Signature.
2230: // The verification returns the value of openssl_verify(): 1 (valid),
2231: // 0 (invalid) or -1 (error). -1 is returned, whatever the signature
2232: // value, when the key of the certificate does not suit the signature
2233: // algorithm (e.g. an EC or DSA key with an RSA algorithm), and it is
2234: // TRUE in a boolean context: only a strict 1 makes the signature valid.
2235: try
2236: {
2237: $signatureVerification = $XMLDSig->verify($objKeyX509);
2238: }
2239: catch (\Throwable $e)
2240: {
2241: $signatureVerification = false;
2242: }
2243:
2244: if (1 === $signatureVerification)
2245: {
2246: // Token signature is valid.
2247: $this->isSignatureValueOk = true;
2248: }
2249: else
2250: {
2251: // ANOMALY: Token hash signature verification failed!
2252: $this->isSignatureValueOk = false;
2253: $this->anomalies[] = "Token hash signature verification failed!";
2254: }
2255:
2256: // Neither decrypt nor read a token whose hash or hash signature is not
2257: // valid: its content is not the signed one.
2258: if (!$this->isDigestValueOk || !$this->isSignatureValueOk)
2259: {
2260: $this->_throwAnomalies();
2261: }
2262:
2263: // The signature has only been verified with the certificate the token
2264: // carries, which anyone can produce. Neither decrypt nor read a token
2265: // whose certificate is not trusted.
2266: $this->_verifyCertTrust();
2267:
2268: // Check if the content of the node "ds:Object" is ciphered.
2269: $encryptedDataNode = $this->xpath->query("xenc:EncryptedData", $nodeObject)->item(0);
2270:
2271: if (is_null($encryptedDataNode))
2272: {
2273: // The node "ds:Object" is not ciphered.
2274: // Set encryption flag to FALSE.
2275: $this->isDataEncrypted = false;
2276: }
2277: else
2278: {
2279: // The node "ds:Object" is ciphered.
2280: // Set encryption flag to TRUE.
2281: $this->isDataEncrypted = true;
2282:
2283: // Do not decrypt a token encrypted with other algorithms than
2284: // the expected ones: the decryption itself is what an attacker
2285: // submitting altered tokens probes (see
2286: // _isValidKeyTransportAlgorithm() and
2287: // _isAuthenticatedCipheringAlgorithm()).
2288: if (!empty($this->anomalies))
2289: {
2290: $this->_throwAnomalies();
2291: }
2292:
2293: // A token created without the private key of the recipient
2294: // cannot be decrypted: everything that can be checked without
2295: // decrypting it has been, and its content is left unread.
2296: if ($isCreation && is_null($this->cryptKey))
2297: {
2298: return;
2299: }
2300:
2301: // Decrypt it now.
2302: $this->_decryptToken($encryptedDataNode);
2303: }
2304:
2305: // Verify that the token data is well formed. The token is read from the
2306: // digested node "ds:Signature/ds:Object" (see above).
2307: $queries = array(
2308: self::TOKEN_NAME,
2309: self::TOKEN_NAME . "/" . self::TOKEN_TIMESTAMP_NAME,
2310: self::TOKEN_NAME . "/" . self::TOKEN_DATA_NAME,
2311: );
2312:
2313: foreach ($queries as $query)
2314: {
2315: if ($this->xpath->query($query, $nodeObject)->length !== 1)
2316: {
2317: // ERROR: Malformed token! The structure of an encrypted
2318: // token is that of its decrypted content: it fails as the
2319: // decryption does (see _decryptToken()).
2320: throw new Exception($this->isDataEncrypted ? self::DECRYPTION_FAILED : "Malformed token!");
2321: }
2322: }
2323:
2324: // Get the node "ds:Signature/ds:Object/Token/TokenTimestamp".
2325: $query = self::TOKEN_NAME . "/" . self::TOKEN_TIMESTAMP_NAME;
2326: $nodeTokenTimestamp = $this->xpath->query($query, $nodeObject)->item(0);
2327:
2328: // Check that token timestamp node is a DOM element that contains solely
2329: // a text node.
2330: if (XML_ELEMENT_NODE === $nodeTokenTimestamp->nodeType
2331: && $nodeTokenTimestamp->hasChildNodes()
2332: && 1 === $nodeTokenTimestamp->childNodes->length
2333: && XML_TEXT_NODE === $nodeTokenTimestamp->childNodes->item(0)->nodeType)
2334: {
2335: // Compute the UTC UNIX timestamp from the token timestamp string.
2336: $timezone = new DateTimeZone('UTC');
2337: $UTCDate = DateTime::createFromFormat(self::TOKEN_TIMESTAMP_FORMAT, $nodeTokenTimestamp->nodeValue, $timezone);
2338: if (false !== $UTCDate)
2339: {
2340: // Store token timestamp.
2341: $this->timestamp = $nodeTokenTimestamp->nodeValue;
2342: }
2343: else
2344: {
2345: // ANOMALY: Token timestamp format is incorrect!
2346: $this->anomalies[] = "Token timestamp format is invalid!";
2347: }
2348: }
2349: else
2350: {
2351: // ANOMALY: Malformed token timestamp.
2352: $this->anomalies[] = "Token timestamp is malformed!";
2353: }
2354:
2355: // Get the node "ds:Signature/ds:Object/Token/TokenId". A token without
2356: // identifier (as created by the 0.x versions) is rejected: the
2357: // receiver could not tell it from a replayed one (see getTokenId()).
2358: $query = self::TOKEN_NAME . "/" . self::TOKEN_ID_NAME;
2359: $nodeTokenIds = $this->xpath->query($query, $nodeObject);
2360: if (0 === $nodeTokenIds->length)
2361: {
2362: // ANOMALY: Missing token identifier.
2363: $this->anomalies[] = "Token identifier is missing!";
2364: }
2365: elseif (1 === $nodeTokenIds->length
2366: && 1 === $nodeTokenIds->item(0)->childNodes->length
2367: && XML_TEXT_NODE === $nodeTokenIds->item(0)->firstChild->nodeType
2368: && preg_match('/\A[0-9a-f]{' . (2 * self::TOKEN_ID_BYTES) . '}\z/', $nodeTokenIds->item(0)->nodeValue))
2369: {
2370: // Store token identifier.
2371: $this->tokenId = $nodeTokenIds->item(0)->nodeValue;
2372: }
2373: else
2374: {
2375: // ANOMALY: Malformed token identifier.
2376: $this->anomalies[] = "Token identifier is malformed!";
2377: }
2378:
2379: // Get the node "ds:Signature/ds:Object/Token/TokenExpiry", if any: a
2380: // token created without the option "validity" has none.
2381: $query = self::TOKEN_NAME . "/" . self::TOKEN_EXPIRY_NAME;
2382: $nodeTokenExpiries = $this->xpath->query($query, $nodeObject);
2383: $this->hasExpiryNode = 0 < $nodeTokenExpiries->length;
2384: if ($this->hasExpiryNode)
2385: {
2386: $timezone = new DateTimeZone('UTC');
2387: $expiryDate = (1 === $nodeTokenExpiries->length && 1 === $nodeTokenExpiries->item(0)->childNodes->length && XML_TEXT_NODE === $nodeTokenExpiries->item(0)->firstChild->nodeType)
2388: ? DateTime::createFromFormat(self::TOKEN_TIMESTAMP_FORMAT, $nodeTokenExpiries->item(0)->nodeValue, $timezone)
2389: : false;
2390: $timestampDate = is_null($this->timestamp) ? false : DateTime::createFromFormat(self::TOKEN_TIMESTAMP_FORMAT, $this->timestamp, $timezone);
2391: if (false === $expiryDate || $expiryDate->format(self::TOKEN_TIMESTAMP_FORMAT) !== $nodeTokenExpiries->item(0)->nodeValue)
2392: {
2393: // ANOMALY: Malformed token expiry.
2394: $this->anomalies[] = "Token expiry is malformed!";
2395: }
2396: elseif (false !== $timestampDate && $expiryDate <= $timestampDate)
2397: {
2398: // ANOMALY: Token expiring before it was issued.
2399: $this->anomalies[] = "Token expiry is not after its timestamp!";
2400: }
2401: else
2402: {
2403: // Store token expiry.
2404: $this->expiry = $nodeTokenExpiries->item(0)->nodeValue;
2405: }
2406: }
2407:
2408: // Get the nodes "ds:Signature/ds:Object/Token/TokenAudience", if any,
2409: // and check them against the expected audience (see
2410: // _verifyAudience()) before the token data are read.
2411: $query = self::TOKEN_NAME . "/" . self::TOKEN_AUDIENCE_NAME;
2412: $nodeTokenAudiences = $this->xpath->query($query, $nodeObject);
2413: if (0 < $nodeTokenAudiences->length)
2414: {
2415: $audiences = [];
2416: foreach ($nodeTokenAudiences as $nodeTokenAudience)
2417: {
2418: if (1 !== $nodeTokenAudience->childNodes->length
2419: || XML_TEXT_NODE !== $nodeTokenAudience->firstChild->nodeType
2420: || !self::_isValidAudience($nodeTokenAudience->nodeValue))
2421: {
2422: // ANOMALY: Malformed token audience. The token is
2423: // rejected before its data are read: a malformed
2424: // audience is not one the receiver can be sure of.
2425: $this->anomalies[] = "Token audience is malformed!";
2426: $this->_throwAnomalies();
2427: }
2428: $audiences[] = $nodeTokenAudience->nodeValue;
2429: }
2430: $this->audience = $audiences;
2431: }
2432:
2433: // A token whose timestamp, identifier or expiry is malformed is
2434: // rejected here, before its data are read and before its audience is
2435: // checked, so that the error lists these anomalies.
2436: if (!empty($this->anomalies))
2437: {
2438: $this->_throwAnomalies();
2439: }
2440:
2441: $this->_verifyAudience($isCreation);
2442:
2443: // Get the node "ds:Signature/ds:Object/Token/TokenData".
2444: $query = self::TOKEN_NAME . "/" . self::TOKEN_DATA_NAME;
2445: $nodeTokenData = $this->xpath->query($query, $nodeObject)->item(0);
2446:
2447: /**
2448: * Inline function to build a flat or multi-dimensional associative
2449: * array from a node tree whose leaf values are base64 encoded. The
2450: * values of the leaves are base64 decoded by this function.
2451: *
2452: * NOTE: This function is called recursively if the node tree is more
2453: * than one level deep.
2454: *
2455: * @param string $rootNode The node that holds the node tree.
2456: *
2457: * @param array $data The array to fill up with decoded data.
2458: */
2459: $nodeTreeToData = function($rootNode, &$data) use (&$nodeTreeToData)
2460: {
2461: if (!$rootNode->hasChildNodes())
2462: {
2463: // ANOMALY: Empty token data.
2464: $this->anomalies[] = "Token data is empty!";
2465: }
2466: else
2467: {
2468: foreach ($rootNode->childNodes as $node)
2469: {
2470: if ((XML_TEXT_NODE === $node->nodeType && 1 !== $node->parentNode->childNodes->length))
2471: {
2472: // ANOMALY: Malformed token data.
2473: $this->anomalies[] = "Token data is malformed!";
2474: }
2475: if (XML_ELEMENT_NODE === $node->nodeType && $node->hasChildNodes())
2476: {
2477: if (1 === $node->childNodes->length && XML_TEXT_NODE === $node->childNodes->item(0)->nodeType)
2478: {
2479: $decode = ("base64" === $node->getAttribute('Algorithm')) ? true : false;
2480: $data[$node->nodeName] = $decode ? base64_decode($node->childNodes->item(0)->nodeValue) : $node->childNodes->item(0)->nodeValue;
2481: }
2482: else
2483: {
2484: $data[$node->nodeName] = array();
2485:
2486: // Recursive call.
2487: $nodeTreeToData($node, $data[$node->nodeName]);
2488: }
2489: }
2490: }
2491: }
2492: };
2493:
2494: // Check that token data is well formed and extract data.
2495: $tokenData = array();
2496: $nodeTreeToData($nodeTokenData, $tokenData);
2497:
2498: // Store token data.
2499: $this->data = $tokenData;
2500:
2501: // If any anomaly has been detected during analysis, set an error
2502: // message.
2503: if (!empty($this->anomalies))
2504: {
2505: $this->_throwAnomalies();
2506: }
2507: }
2508:
2509:
2510: /**
2511: * Load an XML document into a DOM document.
2512: *
2513: * A document type declaration (DOCTYPE) declares entities, which the
2514: * parser expands where the document references them: nested internal
2515: * entities make a small document expand into gigabytes ("billion
2516: * laughs"), and external entities read files or URLs. An XML token never
2517: * has one, so a document holding the bytes `<!DOCTYPE` is refused before
2518: * it is parsed, whatever it declares. This check reads the raw bytes: it
2519: * only holds for an encoding in which `<!DOCTYPE` can only be written as
2520: * these very bytes, which is why a document is also refused, before it is
2521: * parsed, when it is not in UTF-8, the encoding of the tokens: one that
2522: * holds a NUL byte (UTF-16, UTF-32), that declares another encoding, or
2523: * that does not start with `<` (EBCDIC, among others). The parsed document is refused too
2524: * if it has a document type node, should the check above miss one.
2525: *
2526: * The document is parsed with `LIBXML_NONET`: the parser never accesses
2527: * the network. Any libxml error refuses the document, and none is output.
2528: * Whitespace is preserved: digests are computed over the document as it
2529: * was signed.
2530: *
2531: * @param string $xml The XML document.
2532: *
2533: * @param string $what What the document is, for the error messages.
2534: *
2535: * @return DOMDocument The loaded document.
2536: *
2537: * @throws Exception If the document is refused.
2538: */
2539: static private function _loadXML($xml, $what)
2540: {
2541: if (!is_string($xml) || '' === $xml)
2542: {
2543: throw new Exception($what . " is not a valid XML document!");
2544: }
2545:
2546: $start = ltrim(preg_replace('/\A\xEF\xBB\xBF/', '', $xml), " \t\r\n");
2547: if (false !== strpos($xml, "\0")
2548: || (preg_match('/\A<\?xml\s[^>]*?\bencoding\s*=\s*(["\'])([^"\']*)\1/', $start, $matches)
2549: && 0 !== strcasecmp($matches[2], 'UTF-8')))
2550: {
2551: throw new Exception($what . " must be encoded in UTF-8!");
2552: }
2553: if ('<' !== substr($start, 0, 1))
2554: {
2555: throw new Exception($what . " is not a valid XML document!");
2556: }
2557:
2558: if (false !== strpos($xml, '<!DOCTYPE'))
2559: {
2560: throw new Exception($what . " must not have a document type declaration (DOCTYPE)!");
2561: }
2562:
2563: $xmlDoc = new DOMDocument('1.0', 'UTF-8');
2564: $xmlDoc->preserveWhiteSpace = true;
2565:
2566: $prevState = libxml_use_internal_errors(true);
2567: libxml_clear_errors();
2568: try
2569: {
2570: $loaded = $xmlDoc->loadXML($xml, LIBXML_NONET);
2571: $isValidXml = $loaded && 0 === count(libxml_get_errors());
2572: }
2573: finally
2574: {
2575: libxml_clear_errors();
2576: libxml_use_internal_errors($prevState);
2577: }
2578:
2579: if (!$isValidXml)
2580: {
2581: throw new Exception($what . " is not a valid XML document!");
2582: }
2583:
2584: foreach ($xmlDoc->childNodes as $node)
2585: {
2586: if (XML_DOCUMENT_TYPE_NODE === $node->nodeType)
2587: {
2588: throw new Exception($what . " must not have a document type declaration (DOCTYPE)!");
2589: }
2590: }
2591:
2592: return $xmlDoc;
2593: }
2594:
2595:
2596: /**
2597: * Decrypt the content of the node "ds:Object" in place.
2598: *
2599: * The failures that do not depend on the token content (no decryption
2600: * key, a key that cannot be loaded) or only on its structure have their
2601: * own message. Every failure that depends on the decrypted content
2602: * (session key, token data, parsing of the decrypted XML) gives the same
2603: * error, `DECRYPTION_FAILED`, without its reason, and raises no PHP
2604: * warning: a message or a warning per failure would tell an attacker who
2605: * can submit altered tokens which step failed, which is how padding
2606: * oracles (Bleichenbacher on RSA 1.5, Jager-Somorovsky on CBC) decrypt a
2607: * token step by step. `_readXML()` only calls this function once the
2608: * digest, the signature, the certificate and the algorithms of the token
2609: * have been verified.
2610: *
2611: * @param DOMElement $encryptedDataNode The node "xenc:EncryptedData",
2612: * child of the digested node "ds:Signature/ds:Object".
2613: *
2614: * @throws Exception
2615: */
2616: private function _decryptToken($encryptedDataNode)
2617: {
2618: // Check that we have a private key to perform decryption.
2619: if (is_null($this->cryptKey))
2620: {
2621: // ERROR: No private key provided for decryption.
2622: throw new Exception("Token is encrypted but no private key is provided for decryption!");
2623: }
2624:
2625: // Passes the node containing the ciphered token to an XMLSecEnc
2626: // object, and indicates to it what is encrypted (the node element or
2627: // only its content) by reading the attribute "Type" of this node.
2628: $objXMLSecEnc = new XMLSecEnc();
2629: $objXMLSecEnc->setNode($encryptedDataNode);
2630: $objXMLSecEnc->type = $encryptedDataNode->getAttribute("Type");
2631:
2632: // Get an XMLSecurityKey object already configured with the algorithm
2633: // which was used to encrypt the token data with the session key.
2634: $objKeyCrypt = $objXMLSecEnc->locateKey();
2635: if (is_null($objKeyCrypt))
2636: {
2637: // ERROR: The algorithm cannot be found.
2638: throw new Exception("Cannot locate session key ciphering algorithm!");
2639: }
2640:
2641: // Retrieve the encrypted session key.
2642: $objKeyInfoCrypt = $objXMLSecEnc->locateKeyInfo($objKeyCrypt);
2643: if (is_null($objKeyInfoCrypt))
2644: {
2645: // ERROR: Session key cannot be found.
2646: throw new Exception("Cannot locate session key!");
2647: }
2648: if (!$objKeyInfoCrypt->isEncrypted)
2649: {
2650: // ERROR: Session key found but it is not encrypted.
2651: throw new Exception("Session key is not encrypted!");
2652: }
2653:
2654: // Loads the private key to be used for deciphering the encrypted
2655: // session key. If a password is provided, use it to access the
2656: // private key.
2657: if (!is_null($this->cryptKeyPassword))
2658: {
2659: $objKeyInfoCrypt->passphrase = $this->cryptKeyPassword;
2660: }
2661: $objKeyInfoCrypt->loadKey($this->cryptKey, false);
2662: if (empty($objKeyInfoCrypt->key))
2663: {
2664: // ERROR: Failed loading private key to decrypt session key!
2665: throw new Exception("Failed loading private key to decrypt session key!");
2666: }
2667:
2668: // Decipher the session key, then the token data with it, in place.
2669: // From here on, every failure gives the same error: exceptions are
2670: // caught, libxml errors are kept internal and PHP warnings are
2671: // intercepted (see above).
2672: $failed = false;
2673: $prevState = libxml_use_internal_errors(true);
2674: libxml_clear_errors();
2675: set_error_handler(function () use (&$failed) {
2676: $failed = true;
2677: return true;
2678: }, E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
2679: try
2680: {
2681: $sessionKey = $objKeyInfoCrypt->encryptedCtx->decryptKey($objKeyInfoCrypt);
2682: $objKeyCrypt->loadKey($sessionKey);
2683: $objXMLSecEnc->decryptNode($objKeyCrypt, true);
2684: if (count(libxml_get_errors()) > 0)
2685: {
2686: $failed = true;
2687: }
2688: }
2689: catch (\Throwable $e)
2690: {
2691: $failed = true;
2692: }
2693: finally
2694: {
2695: restore_error_handler();
2696: libxml_clear_errors();
2697: libxml_use_internal_errors($prevState);
2698: }
2699:
2700: if ($failed)
2701: {
2702: // ERROR: Token decryption failed.
2703: throw new Exception(self::DECRYPTION_FAILED);
2704: }
2705: }
2706:
2707:
2708: /**
2709: * Verify that the X.509 certificate included in the XML token is trusted.
2710: *
2711: * The signature of the token is verified with the certificate the token
2712: * carries: any key pair can produce a valid one, with a certificate that
2713: * copies the names of a legitimate one. The token is only valid if the
2714: * certificate is also trusted:
2715: *
2716: * - A trust anchor is required: CA certificates (option `caCertPath`)
2717: * and/or a pinned certificate (option `signerCertPath`). Without it, no
2718: * certificate is trusted.
2719: *
2720: * - The certificate must be valid at the current date and its key usage
2721: * must allow signing (see `X509Cert::canSign()`).
2722: *
2723: * - If a certificate is pinned, the certificate must be this very
2724: * certificate (same fingerprint).
2725: *
2726: * - If CA certificates are given, OpenSSL must verify the chain of the
2727: * certificate up to them (see `X509Cert::isTrustedBy()`). Only the
2728: * signing certificate is taken from the token: the token cannot supply
2729: * intermediate certificates.
2730: *
2731: * - If a subject is expected (option `expectedSubject`), the subject of
2732: * the certificate must match it.
2733: *
2734: * Sets the flag returned by `isCertTrusted()`.
2735: *
2736: * @throws Exception If the certificate is not trusted, with the reason.
2737: */
2738: private function _verifyCertTrust()
2739: {
2740: $this->isCertTrusted = false;
2741: $cert = $this->x509Certificate;
2742:
2743: if (is_null($this->trustedCACertPaths) && is_null($this->pinnedSignerCert))
2744: {
2745: throw new Exception("No trust anchor! Pass the option 'caCertPath' (trusted CA certificates) and/or 'signerCertPath' (pinned signing certificate) to analyze a token: without them, no signing certificate can be trusted.");
2746: }
2747:
2748: if ($cert->isOutOfDate())
2749: {
2750: throw new Exception("Signing certificate is not valid at the current date (valid from " . $cert->getValidFrom(self::TOKEN_TIMESTAMP_FORMAT) . " to " . $cert->getValidTo(self::TOKEN_TIMESTAMP_FORMAT) . ")!");
2751: }
2752:
2753: if (!$cert->canSign())
2754: {
2755: throw new Exception("Signing certificate is not allowed to sign (key usage)!");
2756: }
2757:
2758: if (!is_null($this->pinnedSignerCert))
2759: {
2760: $pinned = new X509Cert($this->pinnedSignerCert);
2761: if (!hash_equals($pinned->getFingerprint(), $cert->getFingerprint()))
2762: {
2763: throw new Exception("Signing certificate is not the pinned certificate!");
2764: }
2765: }
2766:
2767: if (!is_null($this->trustedCACertPaths) && !$cert->isTrustedBy($this->trustedCACertPaths))
2768: {
2769: throw new Exception("Signing certificate is not issued by a trusted CA: its chain could not be verified up to the CA certificates of the option 'caCertPath', which must hold the issuing CA and the certificates up to its root!");
2770: }
2771:
2772: if (!is_null($this->expectedSubject))
2773: {
2774: $matches = is_string($this->expectedSubject)
2775: ? $cert->getDN() === $this->expectedSubject
2776: : self::_matchesName($cert->getSubject(), $this->expectedSubject);
2777: if (!$matches)
2778: {
2779: throw new Exception("Subject of the signing certificate is not the expected one! Subject: " . $cert->getDN() . ".");
2780: }
2781: }
2782:
2783: $this->isCertTrusted = true;
2784: }
2785:
2786:
2787: /**
2788: * Verify that the token is addressed to the expected audience.
2789: *
2790: * A token signed by a trusted issuer is valid for every receiver that
2791: * trusts this issuer: without an audience, a token issued for one
2792: * receiver is accepted by another. A token may therefore name the
2793: * receivers it is addressed to (option "audience" of its creation), and
2794: * the analysis then requires the receiver to say who it is (option
2795: * "expectedAudience"):
2796: *
2797: * - A token that names audiences is rejected unless `expectedAudience`
2798: * is given and is one of them. Accepting it without this option would
2799: * let any receiver of the issuer accept a token that is not addressed
2800: * to it.
2801: *
2802: * - A token that names no audience is rejected when `expectedAudience` is
2803: * given: the receiver asked for a token addressed to it.
2804: *
2805: * - A token that names no audience is accepted when `expectedAudience` is
2806: * not given, as are the tokens of version 1.0, which have none.
2807: *
2808: * When the object has just created the token, the audiences read must be
2809: * those it wrote.
2810: *
2811: * @param boolean $isCreation TRUE if the object has just created the
2812: * token, FALSE if it analyzes a given one.
2813: *
2814: * @throws Exception If the token is not addressed to the expected
2815: * audience, with the reason.
2816: */
2817: private function _verifyAudience($isCreation)
2818: {
2819: $audiences = is_null($this->audience) ? [] : $this->audience;
2820:
2821: if ($isCreation)
2822: {
2823: if ($audiences !== (is_null($this->audienceToWrite) ? [] : $this->audienceToWrite))
2824: {
2825: throw new Exception("Token audience is not the one written!");
2826: }
2827: return;
2828: }
2829:
2830: if (is_null($this->expectedAudience))
2831: {
2832: if (!empty($audiences))
2833: {
2834: throw new Exception("Token is addressed to an audience ('" . implode("', '", $audiences) . "'), but the option 'expectedAudience' is not given: pass the audience of this receiver to accept it!");
2835: }
2836: return;
2837: }
2838:
2839: if (empty($audiences))
2840: {
2841: throw new Exception("Token is not addressed to any audience, but the audience '" . $this->expectedAudience . "' is expected!");
2842: }
2843:
2844: if (!in_array($this->expectedAudience, $audiences, true))
2845: {
2846: throw new Exception("Token is not addressed to the expected audience! Expected: '" . $this->expectedAudience . "'. Token audience: '" . implode("', '", $audiences) . "'.");
2847: }
2848: }
2849:
2850:
2851: /**
2852: * Indicates whether a name (the subject or the issuer of a certificate,
2853: * as returned by `openssl_x509_parse()`) holds the expected fields.
2854: *
2855: * Each expected field must be present with the very same value: character
2856: * case is significant, and a field repeated in the name (e.g. two "OU")
2857: * is an array of its values.
2858: *
2859: * @param array $name The name to check.
2860: *
2861: * @param array $expected The expected fields.
2862: *
2863: * @return boolean TRUE if every expected field matches, FALSE otherwise.
2864: */
2865: static private function _matchesName($name, $expected)
2866: {
2867: foreach ($expected as $key => $value)
2868: {
2869: if (!isset($name[$key]) || $name[$key] !== $value)
2870: {
2871: return false;
2872: }
2873: }
2874:
2875: return true;
2876: }
2877:
2878:
2879: /**
2880: * Throw an exception whose message lists the anomalies detected during
2881: * the analysis of the XML token.
2882: *
2883: * @throws Exception
2884: */
2885: private function _throwAnomalies()
2886: {
2887: $nb = count($this->anomalies);
2888: throw new Exception($nb . ($nb === 1 ? " anomaly" : " anomalies") . " detected: " . implode(" ; ", $this->anomalies));
2889: }
2890:
2891:
2892: /**
2893: * Indicates whether XML token signature is valid or not.
2894: *
2895: * This means that:
2896: *
2897: * - No error occurred during the analysis of the XML digital signature.
2898: *
2899: * - No anomaly detected during the analysis of the XML digital signature.
2900: *
2901: * - The token hash has been verified and is valid.
2902: *
2903: * - The token hash signature has been verified and is valid.
2904: *
2905: * - The X.509 certificate included in the XML token is trusted: it is
2906: * valid at the current date, it is the pinned certificate and/or its
2907: * chain has been verified up to the trusted CA certificates, and its
2908: * subject is the expected one, if any (see `isCertTrusted()`).
2909: *
2910: * - The token is addressed to the audience given by the option
2911: * `expectedAudience`, if it names audiences or if this option is given
2912: * (see `getAudience()`).
2913: *
2914: * Without a trust anchor given to the analysis, it returns FALSE.
2915: *
2916: * **It checks neither the freshness of the token nor whether it is
2917: * replayed.** A token that was valid once stays valid here, however old
2918: * it is and however many times it is submitted. Before trusting the data
2919: * of a received token, the caller must check that:
2920: *
2921: * - `isSignatureValid()` returns TRUE;
2922: *
2923: * - `isOutOfDate($ttl)` returns FALSE, for the time to live the
2924: * application accepts (it also checks the expiry of the token, if
2925: * any);
2926: *
2927: * - the identifier returned by `getTokenId()` has not been accepted
2928: * before: store it atomically, and refuse the token if it already is.
2929: *
2930: * The trust anchor `caCertPath` accepts any certificate its CA issues: if
2931: * this CA also issues certificates to others than the expected sender,
2932: * any of them can sign a token that passes this check. Restrict it then
2933: * to the sender with the option `expectedSubject`, or pin its certificate
2934: * with `signerCertPath`.
2935: *
2936: * For a token created encrypted without the private key of the
2937: * recipient, it tells whether the checks made without decrypting the
2938: * token passed (see `createSecureXMLToken()`).
2939: *
2940: * @return boolean TRUE if XML token is valid, FALSE otherwise.
2941: */
2942: public function isSignatureValid()
2943: {
2944: return true === $this->isDigestValueOk
2945: && true === $this->isSignatureValueOk
2946: && true === $this->isCertTrusted
2947: && is_null($this->error);
2948: }
2949:
2950:
2951: /**
2952: * Indicates whether the X.509 certificate included in the XML token is
2953: * trusted or not, according to the trust anchor given to the analysis
2954: * (options `caCertPath`, `signerCertPath` and `expectedSubject`).
2955: *
2956: * NOTE: If not evaluated (because an error occurred before), this function
2957: * returns NULL. When it returns FALSE, `getError()` gives the reason.
2958: *
2959: * @return boolean|null TRUE if the certificate is trusted, FALSE
2960: * otherwise, NULL if cannot be evaluated.
2961: */
2962: public function isCertTrusted()
2963: {
2964: return $this->isCertTrusted;
2965: }
2966:
2967:
2968: /**
2969: * Indicates whether the XML token is out of date or not.
2970: *
2971: * The token is out of date if its timestamp is more than `$ttl` seconds
2972: * in the past, or more than `DESYNC_TOLERANCE` seconds in the future
2973: * (clocks of sender and receiver out of sync), or if it has no valid
2974: * timestamp. A token created with the option `validity` is also out of
2975: * date once its expiry is passed (see `getExpiry()`): the shorter of the
2976: * validity set by the issuer and the `$ttl` set by the receiver applies.
2977: *
2978: * The expiry is only checked here, with the timestamp: like the time to
2979: * live, it is a check left to the caller, and `isSignatureValid()` does
2980: * not make it. `DESYNC_TOLERANCE` does not apply to the expiry: a
2981: * receiver whose clock is ahead rejects the token a little earlier.
2982: * Version 1.0 of this class ignores the expiry: a receiver that has not
2983: * upgraded only applies its own `$ttl`.
2984: *
2985: * NOTE: A token that is not out of date can still be a replayed one: see
2986: * `getTokenId()`.
2987: *
2988: * @param int $ttl The time to live (in seconds) allowed for the token.
2989: *
2990: * @return boolean TRUE if token is out of date, FALSE otherwise.
2991: */
2992: public function isOutOfDate($ttl = self::TOKEN_DEFAULT_TTL)
2993: {
2994: if (is_null($this->timestamp))
2995: {
2996: // Timestamp is missing! So we assume token is expired.
2997: return true;
2998: }
2999:
3000: // We will compare date using UTC timezone.
3001: $timezone = new DateTimeZone('UTC');
3002:
3003: // Compute the UTC UNIX timestamp from the token timestamp string.
3004: $tokenUTCDate = DateTime::createFromFormat(self::TOKEN_TIMESTAMP_FORMAT, $this->timestamp, $timezone);
3005: if (false === $tokenUTCDate)
3006: {
3007: // Token timestamp format is incorrect! So we assume token is
3008: // expired.
3009: return true;
3010: }
3011: $tokenUTCTimestamp = $tokenUTCDate->getTimestamp();
3012:
3013: // Compute the UTC UNIX timestamp for the current date/time.
3014: $curUTCDate = new DateTime('now', $timezone);
3015: $curUTCTimestamp = $curUTCDate->getTimestamp();
3016:
3017: // A token whose expiry is passed is out of date, whatever $ttl. So
3018: // is a token whose expiry could not be read: it is not valid either
3019: // (see isSignatureValid()), but a caller that only calls this
3020: // function must not take it for a token without expiry.
3021: if ($this->hasExpiryNode && is_null($this->expiry))
3022: {
3023: return true;
3024: }
3025: if (!is_null($this->expiry))
3026: {
3027: $tokenUTCExpiry = DateTime::createFromFormat(self::TOKEN_TIMESTAMP_FORMAT, $this->expiry, $timezone);
3028: if (false === $tokenUTCExpiry || $tokenUTCExpiry->getTimestamp() < $curUTCTimestamp)
3029: {
3030: return true;
3031: }
3032: }
3033:
3034: // Return timestamps comparison result.
3035: return $tokenUTCTimestamp > ($curUTCTimestamp + self::DESYNC_TOLERANCE) || ($tokenUTCTimestamp + $ttl) < $curUTCTimestamp;
3036: }
3037:
3038:
3039: /**
3040: * Indicates whether token data is encrypted or not.
3041: *
3042: * For a token created encrypted, it returns TRUE, whether or not the
3043: * private key of the recipient was given (see `createSecureXMLToken()`).
3044: *
3045: * NOTE: If not evaluated (because an error occurred before), this function
3046: * returns NULL.
3047: *
3048: * @return boolean|null TRUE if token data is encrypted, FALSE otherwise,
3049: * NULL if cannot be evaluated.
3050: */
3051: public function isDataEncrypted()
3052: {
3053: return $this->isDataEncrypted;
3054: }
3055:
3056:
3057: /**
3058: * Indicates whether the digest of token data (a hash) has been verified and
3059: * is valid or not.
3060: *
3061: * NOTE: If not evaluated (because an error occurred before), this function
3062: * returns NULL.
3063: *
3064: * @return boolean|null TRUE if token hash has been verified and is valid,
3065: * FALSE otherwise, NULL if cannot be evaluated.
3066: */
3067: public function isDigestValueOk()
3068: {
3069: return $this->isDigestValueOk;
3070: }
3071:
3072:
3073: /**
3074: * Indicates whether signature of the digest of the token data has been
3075: * verified and is valid or not.
3076: *
3077: * NOTE: If not evaluated (because an error occurred before), this function
3078: * returns NULL.
3079: *
3080: * @return boolean|null TRUE if token hash signature has been verified and
3081: * is valid, FALSE otherwise, NULL if cannot be evaluated.
3082: */
3083: public function isSignatureValueOk()
3084: {
3085: return $this->isSignatureValueOk;
3086: }
3087:
3088:
3089: /**
3090: * Get the XML token.
3091: *
3092: * @return string|null The XML token, NULL if not available.
3093: */
3094: public function getXML()
3095: {
3096: return $this->xml;
3097: }
3098:
3099:
3100: /**
3101: * Get the user data contained in the XML token.
3102: *
3103: * The data can only be trusted if `isSignatureValid()` returns TRUE (and
3104: * the token is neither out of date nor replayed). They are only read
3105: * once the digest, the signature and the signing certificate have been
3106: * verified.
3107: *
3108: * A token created encrypted without the private key of the recipient is
3109: * not decrypted: its data are not available (see
3110: * `createSecureXMLToken()`).
3111: *
3112: * @return array|null The associative array containing token data, NULL if
3113: * not available.
3114: */
3115: public function getData()
3116: {
3117: return $this->data;
3118: }
3119:
3120:
3121: /**
3122: * Get the timestamp of the XML token.
3123: *
3124: * The timestamp of a token that is not valid is not given: nothing read
3125: * from such a token can be trusted. `isOutOfDate()` still reads it, and
3126: * rejects such a token anyway when its timestamp could not be read.
3127: *
3128: * @return string|null The token timestamp, NULL if the token is not
3129: * valid (see `isSignatureValid()`), or if it was created encrypted
3130: * without the private key of the recipient, which leaves its content
3131: * unread (see `createSecureXMLToken()`).
3132: */
3133: public function getTimestamp()
3134: {
3135: return $this->isSignatureValid() ? $this->timestamp : null;
3136: }
3137:
3138:
3139: /**
3140: * Get the identifier of the XML token.
3141: *
3142: * Each token created by this class holds a random identifier of 128 bits
3143: * (32 hexadecimal characters), generated with `random_bytes()` and
3144: * covered by the signature. A valid token can be submitted again for as
3145: * long as `isOutOfDate()` accepts it: the class keeps no list of the
3146: * tokens it has analyzed, so the caller must refuse a token whose
3147: * identifier it has already accepted. A token is accepted from
3148: * `DESYNC_TOLERANCE` seconds before its timestamp to `$ttl` seconds after
3149: * it (see `isOutOfDate()`): keep each accepted identifier for at least
3150: * `$ttl + XMLDSigToken::DESYNC_TOLERANCE` seconds after accepting it.
3151: *
3152: * A token without identifier (as created by the 0.x versions) is not
3153: * valid.
3154: *
3155: * @return string|null The token identifier, NULL if the token is not
3156: * valid (see `isSignatureValid()`), or if it was created encrypted
3157: * without the private key of the recipient, which leaves its content
3158: * unread (see `createSecureXMLToken()`).
3159: */
3160: public function getTokenId()
3161: {
3162: return $this->isSignatureValid() ? $this->tokenId : null;
3163: }
3164:
3165:
3166: /**
3167: * Get the audiences the XML token is addressed to.
3168: *
3169: * A token created with the option `audience` names the receivers it is
3170: * addressed to, and `isSignatureValid()` only accepts it if the analysis
3171: * gives one of them with the option `expectedAudience` (see
3172: * `__construct()`).
3173: *
3174: * @return array|null The list of the audiences of the token, NULL if the
3175: * token is not valid (see `isSignatureValid()`), if it names no
3176: * audience, or if it was created encrypted without the private key of
3177: * the recipient, which leaves its content unread (see
3178: * `createSecureXMLToken()`).
3179: */
3180: public function getAudience()
3181: {
3182: return $this->isSignatureValid() ? $this->audience : null;
3183: }
3184:
3185:
3186: /**
3187: * Get the expiry of the XML token.
3188: *
3189: * A token created with the option `validity` holds the date after which
3190: * it is out of date (see `isOutOfDate()`), in the format of the timestamp
3191: * (`TOKEN_TIMESTAMP_FORMAT`, UTC).
3192: *
3193: * @return string|null The token expiry, NULL if the token is not valid
3194: * (see `isSignatureValid()`), if it has no expiry, or if it was
3195: * created encrypted without the private key of the recipient, which
3196: * leaves its content unread (see `createSecureXMLToken()`).
3197: */
3198: public function getExpiry()
3199: {
3200: return $this->isSignatureValid() ? $this->expiry : null;
3201: }
3202:
3203:
3204: /**
3205: * Get the message of the error that occurred while parsing the XML token.
3206: *
3207: * @return string|null The error message. If no error, NULL is returned.
3208: */
3209: public function getError()
3210: {
3211: return $this->error;
3212: }
3213:
3214:
3215: /**
3216: * Get the list of anomalies that occurred while parsing the XML token.
3217: *
3218: * @return array The list of anomalies, empty if none.
3219: */
3220: public function getAnomalies()
3221: {
3222: return $this->anomalies;
3223: }
3224:
3225:
3226: /**
3227: * Get the X.509 certificate that is included in the XML token (PEM format).
3228: *
3229: * @return string|null The content of the X.509 certificate, NULL if not
3230: * available.
3231: */
3232: public function getCertificate()
3233: {
3234: if (is_null($this->x509Certificate))
3235: {
3236: return null;
3237: }
3238:
3239: return $this->x509Certificate->getPem();
3240: }
3241:
3242:
3243: /**
3244: * Get the issuer information of the X.509 certificate that is included in
3245: * the XML token.
3246: *
3247: * @return array|null The issuer information of the X.509 certificate, NULL
3248: * if not available.
3249: */
3250: public function getCertIssuer()
3251: {
3252: if (is_null($this->x509Certificate))
3253: {
3254: return null;
3255: }
3256:
3257: return $this->x509Certificate->getIssuer();
3258: }
3259:
3260:
3261: /**
3262: * Get the subject information of the X.509 certificate that is included in
3263: * the XML token.
3264: *
3265: * @return array|null The subject information of the X.509 certificate, NULL
3266: * if not available.
3267: */
3268: public function getCertSubject()
3269: {
3270: if (is_null($this->x509Certificate))
3271: {
3272: return null;
3273: }
3274:
3275: return $this->x509Certificate->getSubject();
3276: }
3277:
3278:
3279: /**
3280: * Get the Distinguished Name of the X.509 certificate that is included in
3281: * the XML token.
3282: *
3283: * @return string|null The Distinguished Name of the X.509 certificate, NULL
3284: * if not available.
3285: */
3286: public function getCertDN()
3287: {
3288: if (is_null($this->x509Certificate))
3289: {
3290: return null;
3291: }
3292:
3293: return $this->x509Certificate->getDN();
3294: }
3295:
3296:
3297: /**
3298: * Get the UTC date from which the X.509 certificate that is included in the
3299: * XML token is valid.
3300: *
3301: * @param string $dateFormat The format of the returned date (Default:
3302: * XMLDSigToken::TOKEN_TIMESTAMP_FORMAT).
3303: *
3304: * @return string|null|false The date from which the X.509 certificate is
3305: * valid, NULL if not available, FALSE if $dateFormat is invalid.
3306: */
3307: public function getCertValidFrom($dateFormat = self::TOKEN_TIMESTAMP_FORMAT)
3308: {
3309: if (is_null($this->x509Certificate))
3310: {
3311: return null;
3312: }
3313:
3314: return $this->x509Certificate->getValidFrom($dateFormat);
3315: }
3316:
3317:
3318: /**
3319: * Get the UTC date to which the X.509 certificate that is included in the
3320: * XML token is valid.
3321: *
3322: * @param string $dateFormat The format of the returned date (Default:
3323: * XMLDSigToken::TOKEN_TIMESTAMP_FORMAT).
3324: *
3325: * @return string|null|false The date to which the X.509 certificate is
3326: * valid, NULL if
3327: * not available, FALSE if $dateFormat is invalid.
3328: */
3329: public function getCertValidTo($dateFormat = self::TOKEN_TIMESTAMP_FORMAT)
3330: {
3331: if (is_null($this->x509Certificate))
3332: {
3333: return null;
3334: }
3335:
3336: return $this->x509Certificate->getValidTo($dateFormat);
3337: }
3338:
3339:
3340: /**
3341: * Indicates whether the X.509 certificate included in the XML token is out
3342: * of date or not.
3343: *
3344: * @return boolean TRUE if X.509 certificate is out of date, FALSE
3345: * otherwise.
3346: */
3347: public function isCertOutOfDate()
3348: {
3349: if (is_null($this->x509Certificate))
3350: {
3351: return true;
3352: }
3353:
3354: return $this->x509Certificate->isOutOfDate();
3355: }
3356:
3357:
3358: /**
3359: * Check that the issuer information of the X.509 certificate that is
3360: * included in the XML token matches the expected one.
3361: *
3362: * The expected issuer information must be passed as an array to the
3363: * function:
3364: *
3365: * ```php
3366: * $ExpectedIssuerInfo = [
3367: * 'C' => 'DK',
3368: * 'ST' => 'Jylland',
3369: * 'O' => 'Lothbrok Ltd',
3370: * 'OU' => 'Jarl Dept',
3371: * 'CN' => 'www.Lothbrok.dk',
3372: * 'emailAddress' => 'ragnar@Lothbrok.dk'
3373: * ];
3374: * ```
3375: *
3376: * NOTE: Character case of data is significant.
3377: *
3378: * NOTE: This only compares names, which any certificate can copy: it
3379: * tells nothing about who issued the certificate. `isSignatureValid()`
3380: * verifies the certificate against the trust anchor of the analysis.
3381: *
3382: * @param array $expectedIssuerInfo The issuer information that the X.509
3383: * certificate should match.
3384: *
3385: * @return boolean TRUE if the issuer of the X.509 certificate holds the
3386: * expected information, FALSE otherwise.
3387: *
3388: * @throws Exception
3389: */
3390: public function isValidCertIssuer($expectedIssuerInfo)
3391: {
3392: // Check if the expected issuer information is an array.
3393: if (!is_array($expectedIssuerInfo))
3394: {
3395: throw new Exception("Invalid parameter 'issuerInfo'! Issuer information should be an array.");
3396: }
3397:
3398: // Verify that the information of the X.509 certificate matches the
3399: // expected one.
3400: $certIssuerInfo = $this->getCertIssuer();
3401: if (is_null($certIssuerInfo))
3402: {
3403: return false;
3404: }
3405:
3406: return self::_matchesName($certIssuerInfo, $expectedIssuerInfo);
3407: }
3408:
3409:
3410: /**
3411: * Check that the X.509 certificate included in the XML token comes from the
3412: * expected CA.
3413: *
3414: * Note that more than one CA certificate can give a positive result, some
3415: * CAs re-issue signing certificates after having only changed the
3416: * expiration dates.
3417: *
3418: * Note that it also works with self-signed certificates. In this case,
3419: * pass the X.509 certificate that is supposed to be included in the XML
3420: * token to the function.
3421: *
3422: * Only the link between the certificate and the given CA certificate is
3423: * checked (see `X509Cert::isValidCA()`). `isSignatureValid()` verifies the
3424: * whole chain against the trust anchor of the analysis.
3425: *
3426: * @param string $caCertPath The PEM certificate (public key) that is
3427: * supposed to be used by the CA to sign the X.509 certificate included in
3428: * the XML token.
3429: *
3430: * @return boolean TRUE if the X.509 certificate has been signed by the
3431: * expected CA, FALSE otherwise (and when no certificate has been read
3432: * from the token).
3433: *
3434: * @throws Exception
3435: */
3436: public function isValidCertCA($caCertPath)
3437: {
3438: if (is_null($this->x509Certificate))
3439: {
3440: return false;
3441: }
3442:
3443: // Get the CA public key that is supposed to be used to sign the X.509
3444: // certificate.
3445: $caCert = @file_get_contents($caCertPath);
3446: if (false === $caCert)
3447: {
3448: throw new Exception("Cannot read CA certificate to perform signer check! File: " . $caCertPath);
3449: }
3450:
3451: return $this->x509Certificate->isValidCA($caCert);
3452: }
3453:
3454:
3455: /**
3456: * Create the XMLSecurityDSig object used to verify an XML token.
3457: *
3458: * Digests and signatures are computed over canonicalized XML. If the
3459: * canonicalization fails, `DOMNode::C14N()` returns FALSE (on PHP 8, some
3460: * XPath failures throw an Error instead), and an XPath transform that
3461: * selects nothing yields an empty string. Depending on its version,
3462: * xmlseclibs then hashes an empty string: a forged reference only has to
3463: * carry the digest of the empty string to validate, whatever the content
3464: * of the referenced node. The returned object therefore makes the
3465: * canonicalization of the node "ds:SignedInfo" and of each referenced node
3466: * throw an exception unless it produces a non-empty string.
3467: *
3468: * This check is deliberately independent of the xmlseclibs version and
3469: * must not be removed, even if xmlseclibs performs a similar check.
3470: *
3471: * @return XMLSecurityDSig The verifier.
3472: */
3473: static private function _createVerifier()
3474: {
3475: return new class() extends XMLSecurityDSig
3476: {
3477: public function canonicalizeSignedInfo()
3478: {
3479: return $this->guard(function () {
3480: return parent::canonicalizeSignedInfo();
3481: }, "ds:SignedInfo");
3482: }
3483:
3484: public function processTransforms($refNode, $objData, $includeCommentNodes = true)
3485: {
3486: return $this->guard(function () use ($refNode, $objData, $includeCommentNodes) {
3487: return parent::processTransforms($refNode, $objData, $includeCommentNodes);
3488: }, "referenced node");
3489: }
3490:
3491: private function guard(callable $canonicalize, $what)
3492: {
3493: try
3494: {
3495: $data = $canonicalize();
3496: }
3497: catch (\Throwable $e)
3498: {
3499: throw new Exception("Canonicalization of " . $what . " failed! Reason: " . $e->getMessage());
3500: }
3501:
3502: if (!is_string($data) || '' === $data)
3503: {
3504: throw new Exception("Canonicalization of " . $what . " failed or is empty!");
3505: }
3506:
3507: return $data;
3508: }
3509: };
3510: }
3511:
3512:
3513: /**
3514: * Check if an algorithm is valid to perform XML canonicalization
3515: * operations.
3516: *
3517: * @param string $algorithm The algorithm reference (URL).
3518: *
3519: * @return boolean TRUE if valid, FALSE otherwise.
3520: */
3521: static private function _isValidCanonicalizationAlgorithm($algorithm)
3522: {
3523: // switch compares loosely: TRUE would match any algorithm.
3524: if (!is_string($algorithm))
3525: {
3526: return(false);
3527: }
3528:
3529: switch ($algorithm)
3530: {
3531: case (self::C14N):
3532: case (self::C14N_COMMENTS):
3533: case (self::EXC_C14N):
3534: case (self::EXC_C14N_COMMENTS):
3535: return(true);
3536: default:
3537: return(false);
3538: }
3539: }
3540:
3541:
3542: /**
3543: * Check if an algorithm is valid to perform hash operations.
3544: *
3545: * @param string $algorithm The algorithm reference (URL).
3546: *
3547: * @return boolean TRUE if valid, FALSE otherwise.
3548: */
3549: static private function _isValidHashAlgorithm($algorithm)
3550: {
3551: // switch compares loosely: TRUE would match any algorithm.
3552: if (!is_string($algorithm))
3553: {
3554: return(false);
3555: }
3556:
3557: switch ($algorithm)
3558: {
3559: case (self::SHA1):
3560: case (self::SHA256):
3561: case (self::SHA384):
3562: case (self::SHA512):
3563: case (self::RIPEMD160):
3564: return(true);
3565: default:
3566: return(false);
3567: }
3568: }
3569:
3570:
3571: /**
3572: * Check if an algorithm is valid to sign the node "ds:SignedInfo".
3573: *
3574: * Signature and key transport algorithms (see
3575: * `_isValidKeyTransportAlgorithm()`) are both asymmetric, but each list
3576: * only holds its own: a key transport algorithm is not a signature
3577: * algorithm, and the reverse.
3578: *
3579: * @param string $algorithm The algorithm reference (URL).
3580: *
3581: * @return boolean TRUE if valid, FALSE otherwise.
3582: */
3583: static private function _isValidSignatureAlgorithm($algorithm)
3584: {
3585: // switch compares loosely: TRUE would match any algorithm.
3586: if (!is_string($algorithm))
3587: {
3588: return(false);
3589: }
3590:
3591: switch ($algorithm)
3592: {
3593: // case (self::DSA_SHA1): // Does not work.
3594: case (self::RSA_SHA1):
3595: case (self::RSA_SHA256):
3596: case (self::RSA_SHA384):
3597: case (self::RSA_SHA512):
3598: // case (self::HMAC_SHA1): // Does not work.
3599: return(true);
3600: default:
3601: return(false);
3602: }
3603: }
3604:
3605:
3606: /**
3607: * Check if an algorithm is valid to encrypt the session key that encrypts
3608: * the token (key transport).
3609: *
3610: * RSA 1.5 (PKCS #1 v1.5 padding) is not valid, neither to create nor to
3611: * analyze a token: whether the padding of a submitted session key is
3612: * valid can be told from the decryption (Bleichenbacher), which lets an
3613: * attacker who can submit altered tokens decrypt a session key.
3614: *
3615: * @param string $algorithm The algorithm reference (URL).
3616: *
3617: * @return boolean TRUE if valid, FALSE otherwise.
3618: *
3619: * @see _isValidSignatureAlgorithm()
3620: */
3621: static private function _isValidKeyTransportAlgorithm($algorithm)
3622: {
3623: // switch compares loosely: TRUE would match any algorithm.
3624: if (!is_string($algorithm))
3625: {
3626: return(false);
3627: }
3628:
3629: switch ($algorithm)
3630: {
3631: case (self::RSA_OAEP_MGF1P):
3632: return(true);
3633: default:
3634: return(false);
3635: }
3636: }
3637:
3638:
3639: /**
3640: * Check if an algorithm is valid to perform symmetric ciphering operations.
3641: *
3642: * @param string $algorithm The algorithm reference (URL).
3643: *
3644: * @return boolean TRUE if valid, FALSE otherwise.
3645: */
3646: static private function _isValidSymmetricCipheringAlgorithm($algorithm)
3647: {
3648: // switch compares loosely: TRUE would match any algorithm.
3649: if (!is_string($algorithm))
3650: {
3651: return(false);
3652: }
3653:
3654: switch ($algorithm)
3655: {
3656: case (self::TRIPLEDES_CBC):
3657: case (self::AES128_CBC):
3658: case (self::AES192_CBC):
3659: case (self::AES256_CBC):
3660: case (self::AES128_GCM):
3661: case (self::AES192_GCM):
3662: case (self::AES256_GCM):
3663: return(true);
3664: default:
3665: return(false);
3666: }
3667: }
3668:
3669:
3670: /**
3671: * Check if a symmetric algorithm authenticates the data it decrypts.
3672: *
3673: * AES-GCM checks an authentication tag before returning any decrypted
3674: * data: altered data fails as a whole. CBC has no integrity: altered
3675: * data is decrypted all the same, and how the decrypted content then
3676: * fails (padding, XML parsing) can tell an attacker who can submit
3677: * altered tokens what it holds (Jager-Somorovsky). A token encrypted
3678: * with CBC is therefore only decrypted if the analysis declares this
3679: * algorithm (option `sessionKeyCipheringAlgorithm`), whatever
3680: * `checkCryptingAlgorithms`.
3681: *
3682: * @param string $algorithm The algorithm reference (URL).
3683: *
3684: * @return boolean TRUE if the algorithm is authenticated, FALSE otherwise.
3685: */
3686: static private function _isAuthenticatedCipheringAlgorithm($algorithm)
3687: {
3688: return in_array($algorithm, [self::AES128_GCM, self::AES192_GCM, self::AES256_GCM], true);
3689: }
3690:
3691:
3692: /**
3693: * Utility function for modifying a node of an XML content.
3694: *
3695: * The XML content is loaded as an XML token is: a document with a
3696: * document type declaration (DOCTYPE), or not encoded in UTF-8, is
3697: * refused before it is parsed, and the parser never accesses the network
3698: * (see `_loadXML()`). A refused or invalid document is left unchanged and
3699: * the function returns FALSE.
3700: *
3701: * @param string $xml XML content, passed by reference.
3702: *
3703: * @param string $nodePath The xpath query to select the node to modify.
3704: * This should be a full path that identifies a single node.
3705: *
3706: * @param string $nodeAttribute In case you want to modify an attribute of
3707: * the specified node, provide its name here, otherwise set this
3708: * parameter to NULL.
3709: *
3710: * @param string $newValue The value that will replace the original value of
3711: * the node or of the attribute of the node if $nodeAttribute parameter
3712: * is provided.
3713: *
3714: * @param boolean $delete If TRUE delete the specified node or attribute.
3715: *
3716: * @return string|false The value of the node or attribute before
3717: * modification or deletion, FALSE otherwise.
3718: */
3719: static public function alterXML(&$xml, $nodePath, $nodeAttribute, $newValue, $delete = false)
3720: {
3721: // Assume modification will fail.
3722: $oldValue = false;
3723:
3724: // Load the XML content into a DOM document.
3725: try
3726: {
3727: $xmlDoc = self::_loadXML($xml, "XML content");
3728: }
3729: catch (Exception $e)
3730: {
3731: return false;
3732: }
3733: $xmlDoc->formatOutput = false;
3734:
3735: // Instantiate a DOMXpath object to navigate in the XML document.
3736: $xpath = new DOMXPath($xmlDoc);
3737: $xpath->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);
3738: $xpath->registerNamespace('xenc', XMLSecEnc::XMLENCNS);
3739:
3740: // Locate the node.
3741: $nodeList = $xpath->query($nodePath);
3742: if (1 <= $nodeList->length)
3743: {
3744: // Node found.
3745: $node = $nodeList->item(0);
3746:
3747: if (is_null($nodeAttribute))
3748: {
3749: if ($delete)
3750: {
3751: // Delete the node.
3752: $oldValue = $node->nodeValue;
3753: $node->parentNode->removeChild($node);
3754: }
3755: else
3756: {
3757: // Replace node value with the given one.
3758: $oldValue = $node->nodeValue;
3759: $node->nodeValue = $newValue;
3760: }
3761: }
3762: else
3763: {
3764: if ($node->hasAttribute($nodeAttribute))
3765: {
3766: if ($delete)
3767: {
3768: // Delete attribute.
3769: $oldValue = $node->getAttribute($nodeAttribute);
3770: $node->removeAttribute($nodeAttribute);
3771: }
3772: else
3773: {
3774: // Replace attribute value with the given one.
3775: $oldValue = $node->getAttribute($nodeAttribute);
3776: $node->removeAttribute($nodeAttribute);
3777: $node->setAttribute($nodeAttribute, $newValue);
3778: }
3779: }
3780: }
3781: }
3782:
3783: // Update the XML content with its modified version.
3784: if ($oldValue)
3785: {
3786: $xml = $xmlDoc->saveXML();
3787: }
3788:
3789: // Return the old value of the modified or deleted element (a node or
3790: // an attribute) or false on failure.
3791: return $oldValue;
3792: }
3793:
3794:
3795: /**
3796: * Return the XML token in a pretty readable format.
3797: *
3798: * This function is intended for display purposes only. Use the `getXML()`
3799: * function to obtain the raw XML token you want to work with.
3800: *
3801: * @return string The pretty formatted XMLDSigToken signature.
3802: */
3803: public function getPrettyXML()
3804: {
3805: $xml = $this->xml;
3806: $formatted = '';
3807: $pad = 0;
3808: $indent_size = 2;
3809: $matches = array();
3810:
3811: // Inline function that splits the value of a node
3812: // into multiple lines of 64 characters.
3813: $splitNodeValue = function(&$xml, $prefix, $xpathQuery)
3814: {
3815: $xpathQuery = str_replace('ds:', $prefix, $xpathQuery);
3816: $nodeValue = XMLDSigToken::alterXML($xml, $xpathQuery, null, 'placeholer', false);
3817: if (false === $nodeValue)
3818: {
3819: // No such node, or the XML token is not a valid XML document.
3820: return;
3821: }
3822: $nodeValue = PHP_EOL . implode(PHP_EOL, str_split($nodeValue, 64)) . PHP_EOL;
3823: XMLDSigToken::alterXML($xml, $xpathQuery, null, $nodeValue, false);
3824: // Remove XML encoded carriage return introduced when splitting node
3825: // value.
3826: $xml = str_replace('&#xD;', '', $xml);
3827: };
3828:
3829: // Split node values that are too long.
3830: $prefix = $this->xmldsigNsPrefix ? $this->xmldsigNsPrefix . ':' : '';
3831: $splitNodeValue($xml, $prefix, '//ds:Signature/ds:SignatureValue');
3832: $splitNodeValue($xml, $prefix, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate');
3833: $splitNodeValue($xml, $prefix, '//ds:Signature/ds:Object/xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue');
3834: $splitNodeValue($xml, $prefix, '//ds:Signature/ds:Object/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue');
3835:
3836: // Tokenise the XML string.
3837: $xml = preg_replace('/(>)[\s]*(<)(\/*)/', "$1\n$2$3", $xml);
3838:
3839: // Get the first token.
3840: $tok = strtok($xml, "\n");
3841:
3842: // Scan each token and adjust indent based on opening/closing tags.
3843: while ($tok !== false)
3844: {
3845: // Test for the various tag states
3846: if (preg_match('/.+<\/\w[^>]*>$/', $tok, $matches))
3847: {
3848: // Opening and closing tags on same line: no change.
3849: $indent = 0;
3850: }
3851: elseif (preg_match('/^<\/\w/', $tok, $matches))
3852: {
3853: // Closing tag: outdent now.
3854: $pad = $pad - $indent_size;
3855: }
3856: elseif (preg_match('/^<\w[^>]*[^\/]>.*$/', $tok, $matches))
3857: {
3858: // Opening tag: don't pad this one, only subsequent tags.
3859: $indent = $indent_size;
3860: }
3861: else
3862: {
3863: // No indentation needed.
3864: $indent = 0;
3865: }
3866:
3867: // Pad the line with the required number of leading spaces.
3868: $prettyLine = str_pad($tok, strlen($tok) + $pad, ' ', STR_PAD_LEFT);
3869: $formatted .= $prettyLine . "\n";
3870:
3871: // Get the next token
3872: $tok = strtok("\n");
3873:
3874: // Update the pad size for subsequent token.
3875: $pad += $indent;
3876: }
3877:
3878: // Return formatted XML string.
3879: return $formatted;
3880: }
3881:
3882:
3883: /**
3884: * Utility function that returns information on the XML token in HTML
3885: * format.
3886: *
3887: * The output shows what the token carries, valid or not: data,
3888: * certificate fields and error messages come from whoever sent the token.
3889: * Every value is therefore escaped with `htmlspecialchars()`
3890: * (`ENT_QUOTES | ENT_SUBSTITUTE`, UTF-8), the outputs of `var_dump()`
3891: * included, so that it can be displayed as is in an HTML page. The output
3892: * loads no external resource (script, style sheet, image).
3893: *
3894: * @return string An HTML fragment.
3895: */
3896: public function getHTMLDump()
3897: {
3898: // Inline function to escape a value for HTML output.
3899: $escape = function($value)
3900: {
3901: return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
3902: };
3903:
3904: // Inline function to output var_dump() into a string.
3905: $dumpToString = function($var)
3906: {
3907: // Capture var_dump() output. When Xdebug overloads var_dump(), it
3908: // writes HTML if html_errors is on: turn it off so that the
3909: // output is plain text, which is then escaped like any value.
3910: $htmlErrors = ini_set('html_errors', '0');
3911: ob_start();
3912: var_dump($var);
3913: $res = ob_get_clean();
3914: if (false !== $htmlErrors)
3915: {
3916: ini_set('html_errors', $htmlErrors);
3917: }
3918:
3919: // Remove the location ("file:line:") Xdebug writes first.
3920: if (extension_loaded('xdebug'))
3921: {
3922: $res = preg_replace('@\A[^\n]*:\d+:\n@', '', $res, 1);
3923: }
3924:
3925: // Return result.
3926: return $res;
3927: };
3928:
3929: $results = array();
3930: $results['isSignatureValid()'] = $this->isSignatureValid() ? 'TRUE' : 'FALSE';
3931: $results['isDataEncrypted()'] = $this->isDataEncrypted() ? 'TRUE' : 'FALSE';
3932: $results['getTimestamp()'] = $this->getTimestamp();
3933: $results['getTokenId()'] = is_null($this->getTokenId()) ? 'NULL' : $this->getTokenId();
3934: $results['getAudience()'] = $dumpToString($this->getAudience());
3935: $results['getExpiry()'] = is_null($this->getExpiry()) ? 'NULL' : $this->getExpiry();
3936: $results['getData()'] = $dumpToString($this->getData());
3937: $results['getError()'] = is_null($this->getError()) ? 'NULL' : $this->getError();
3938: $results['getAnomalies()'] = $dumpToString($this->getAnomalies());
3939: $results['getCertDN()'] = $this->getCertDN();
3940: $results['getCertSubject()'] = $dumpToString($this->getCertSubject());
3941: $results['getCertIssuer()'] = $dumpToString($this->getCertIssuer());
3942: $results['getCertValidFrom()'] = $this->getCertValidFrom();
3943: $results['getCertValidTo()'] = $this->getCertValidTo();
3944: $results['isCertOutOfDate()'] = $this->isCertOutOfDate() ? 'TRUE' : 'FALSE';
3945:
3946: $html = '<table cellpadding="5">';
3947: $html .= '<tr><td colspan="2"><pre class="xdebug-var-dump"><b><span style="color: red">XML Digital Signature</span></b></pre></td></tr>';
3948: $html .= '<tr><td colspan="2"><pre>' . $escape($this->getPrettyXML()) . '</pre></td></tr>';
3949: $html .= '<tr><td width="1%"><pre><b><span style="color: red">METHOD CALLS</span></b></pre></td>';
3950: $html .= '<td valign="top" width="99%"><pre><b><span style="color: red">RESULTS</span></b></pre></td></tr>';
3951: foreach ($results as $key => $value) {
3952: $html .= '<tr><td valign="top"><pre><b>' . $escape($key) . '</b></pre></td>';
3953: $html .= '<td valign="top"><pre>' . $escape($value) . '</pre></td></tr>';
3954: }
3955: $html .= '</table>';
3956:
3957: return $html;
3958: }
3959: }
3960: