├── .gitignore
├── LICENSE.txt
├── README.md
├── composer.json
├── examples
├── keys
│ ├── private.pem
│ ├── public.pem
│ └── public.xml
├── rsa_blob_example.php
└── rsa_dom_example.php
├── phpunit.xml
├── src
└── XmlDigitalSignature.php
└── test
├── XmlDigitalSignatureTest.php
└── data
├── expected-signed.xml
└── keys
├── private.pem
├── public.pem
└── public.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | .buildpath
2 | .project
3 | .settings
4 | .idea
5 | vendor/
6 | composer.lock
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 - Marcel Tyszkiewicz (marcel@webincrement.net)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # XML Digital Signature for PHP
2 |
3 | This library was created to sign arbitrary data and whole XML documents using XML digital signatures as per the [W3 recommendation](http://www.w3.org/TR/xmldsig-core/) using PHP. The code for this class was inspired by the [xmlseclibs library](https://code.google.com/p/xmlseclibs/), which I found impossible to work with due to its lack of documentation and the fact that the signed documents it produced did not validate properly.
4 |
5 | Should this class generate documents that do not validate (as there are many different specs for these signatures, of which I have tested only a handful), please contact me and I will do my best to provide support for your needs.
6 |
7 | # Installation
8 |
9 | Using composer:
10 |
11 | php composer.phar require "marcelxyz/php-xml-digital-signature"
12 |
13 | Alternatively require the `src/XmlDigitalSignature.php` file in your project.
14 |
15 | # Examples
16 |
17 | Here's a basic overview of how to use this library:
18 |
19 | ```php
20 | $dsig = new XmlDsig\XmlDigitalSignature();
21 |
22 | $dsig->loadPrivateKey('path/to/private/key', 'passphrase');
23 | $dsig->loadPublicKey('path/to/public/key');
24 |
25 | $dsig->addObject('I am a data blob.');
26 | $dsig->sign();
27 |
28 | $result = $dsig->getSignedDocument();
29 | ```
30 |
31 | Please see the `examples/` folder for more elaborate examples.
32 |
33 | # API docs
34 |
35 | To sign an XML document you need to answer the following questions:
36 |
37 | 1. Which signature algorithm (RSA/DSA/ECDSA etc.) will you be using?
38 | 2. Which digest (hashing) method will you be using?
39 | 3. Which C14N (canonicalization) method will you be using?
40 | 4. Do you want to include public key information within the resulting XML document?
41 |
42 | These are covered in the following subsections.
43 |
44 | ## Configuration
45 |
46 | ### Signature algorithm
47 |
48 | The following signature algorithms are currently supported:
49 |
50 | - [DSA](https://www.w3.org/TR/xmlsec-algorithms/#DSA) (`XmlDsig\XmlDigitalSignature::DSA_ALGORITHM`)
51 | - [RSA](https://www.w3.org/TR/xmlsec-algorithms/#RSA) (`XmlDsig\XmlDigitalSignature::RSA_ALGORITHM`)
52 | - [Elliptic Curve DSA](https://www.w3.org/TR/xmlsec-algorithms/#ECDSA) (`XmlDsig\XmlDigitalSignature::ECDSA_ALGORITHM`)
53 | - [HMAC](https://www.w3.org/TR/xmlsec-algorithms/#hmac) (`XmlDsig\XmlDigitalSignature::HMAC_ALGORITHM`)
54 |
55 | Specify the appropriate one using the `XmlDsig\XmlDigitalSignature.setCryptoAlgorithm(algo)` method with the appropriate `XmlDsig\XmlDigitalSignature::*_ALGORITHM` constant.
56 |
57 | Default: RSA.
58 |
59 | ### Digest method
60 |
61 | This library currently supports four digest methods, those being:
62 |
63 | - [SHA1](http://www.w3.org/2000/09/xmldsig#sha1) (`XmlDsig\XmlDigitalSignature::DIGEST_SHA1`)
64 | - [SHA256](http://www.w3.org/2001/04/xmlenc#sha256) (`XmlDsig\XmlDigitalSignature::DIGEST_SHA256`)
65 | - [SHA512](http://www.w3.org/2001/04/xmlenc#sha512) (`XmlDsig\XmlDigitalSignature::DIGEST_SHA512`)
66 | - [RIPMED-160](http://www.w3.org/2001/04/xmlenc#ripemd160) (`XmlDsig\XmlDigitalSignature::DIGEST_RIPEMD160`)
67 |
68 | Your version of PHP must provide support for the digest method you choose. This library will check this automatically, but you can also do this yourself by calling PHP's [hash_algos()](http://php.net/manual/en/function.hash-algos.php) function.
69 |
70 | Specify the appropriate digest by calling the `XmlDsig\XmlDigitalSignature.setDigestMethod(digest)` method with the appropriate `XmlDsig\XmlDigitalSignature::DIGEST_*` constant.
71 |
72 | To add support for a different hashing method (provided your version of PHP supports it), add a new `XmlDsig\XmlDigitalSignature::DIGEST_*` const with a value defined in `hash_algos()`. Remember to add the proper mapping values to the following class properties: `$digestMethodUriMapping`, `$openSSLAlgoMapping`, `$digestSignatureAlgoMapping` (read the `@see` notes in the comments of these properties for more information).
73 |
74 | Default: SHA1.
75 |
76 | ### C14N methods
77 |
78 | This lib currently supports the following canonicalization methods:
79 |
80 | - [Canonical XML](http://www.w3.org/TR/2001/REC-xml-c14n-20010315) (`XmlDsig\XmlDigitalSignature::C14N`)
81 | - [Canonical XML with comments](http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) (`XmlDsig\XmlDigitalSignature::C14N_COMMENTS`)
82 | - [Exclusive canonical XML](http://www.w3.org/2001/10/xml-exc-c14n#) (`XmlDsig\XmlDigitalSignature::C14N_EXCLUSIVE`)
83 | - [Exclusive canonical XML with comments](http://www.w3.org/2001/10/xml-exc-c14n#WithComments) (`XmlDsig\XmlDigitalSignature::C14N_EXCLUSIVE_COMMENTS`)
84 |
85 | These can be extended by adding the necessary class constants. If you do add a new C14N method, remember to add its specific options to the `XmlDsig\XmlDigitalSignature::$c14nOptionMapping` array.
86 |
87 | In order to specify a different C14N method, call the `XmlDsig\XmlDigitalSignature.setCanonicalMethod(c14n)` method with the appropriate `XmlDsig\XmlDigitalSignature::C14N_*` constant.
88 |
89 | Default: Canonical XML.
90 |
91 | ### Standalone XML
92 |
93 | To force the resulting XML to contain the standalone pseudo-attribute set to `yes` simply call the `XmlDsig\XmlDigitalSignature.forceStandalone()` method.
94 |
95 | Default: `no`.
96 |
97 | ### Node namespace prefixes
98 |
99 | To specify a different ns prefix (or you don't want to use one at all), simply pass the appropriate value to the `XmlDsig\XmlDigitalSignature.setNodeNsPrefix(prefix)` method.
100 |
101 | Default: `dsig`.
102 |
103 | ## Public/private key generation
104 |
105 | Skip this section and go to [usage](#usage) if your key pairs are already generated.
106 |
107 | There are many ways to generate a key pair, however below are examples of RSA key generation using OpenSSL (unix terminal).
108 |
109 | ### Private RSA key
110 |
111 | openssl genrsa -aes256 -out private.pem 2048
112 |
113 | The above command will generate a private AES256 RSA key with a 2048 modulus. Setting a passphrase is highly recommended.
114 |
115 | ### Public key (PEM format)
116 |
117 | openssl rsa -in private.pem -pubout -out public.pem
118 |
119 | The above command generates a public certificate in PEM format, based on the previously generated (or already existing) private key.
120 |
121 | ### Public key (X.509 format)
122 |
123 | openssl req -x509 -new -key private.pem -days 3650 -out public.crt
124 |
125 | The above command generates a public X.509 certificate valid for 3650 days. You will also be prompted for some trivial information needed to generate this certificate (CSR). The resulting key is also known as a self signed certificate.
126 |
127 | ### Public key (XML format)
128 |
129 | If you need the public key to be attached to the signed XML document in XML format, you will first have to generate a public certificate (either in PEM or X.509 format). Once you have done this, you can convert your key to an XML format.
130 |
131 | Public RSA X.509 certificates can be converted to XML format using [http://tools.ailon.org/tools/XmlKey](http://tools.ailon.org/tools/XmlKey).
132 |
133 | Public RSA PEM certificates, on the other hand, can be converted to XML format using [https://superdry.apphb.com/tools/online-rsa-key-converter](https://superdry.apphb.com/tools/online-rsa-key-converter).
134 |
135 | ## Usage
136 |
137 | Once you have generated your keys and configured the environment then you are ready to start loading keys and adding objects. The methods are explained below.
138 |
139 | ### Loading the generated keys
140 |
141 | Once you have generated the appropriate private, public and XML keys (if necessary), you can load them using the `XmlDsig\XmlDigitalSignature.loadPrivateKey()`, `XmlDsig\XmlDigitalSignature.loadPublicKey()`, `XmlDsig\XmlDigitalSignature.loadPublicXmlKey()` methods, respectively.
142 |
143 | ### Adding objects
144 |
145 | Object data (strings or DOMNodes) can be added to the XML document using the `XmlDsig\XmlDigitalSignature.addObject()` method. If the value of the object needs to be hashed, be sure to pass `true` as the third paramater of the aforementioned method.
146 |
147 | The resulting data will be placed inside of an `` node, and an appropriate `` element set will be generated, containing the digest of the object.
148 |
149 | ### Signing the document
150 |
151 | What may seem trivial by now, you sign the generated XML document using the `XmlDsig\XmlDigitalSignature.sign()` method. Of course, be sure to watch out for the return values of the method and any exceptions it might throw.
152 |
153 | ### Verifying the signatures
154 |
155 | In turn, signatures may be verified using the `XmlDsig\XmlDigitalSignature.verify()` method.
156 |
157 | Additionally you can use the [Aleksey validator](http://www.aleksey.com/xmlsec/xmldsig-verifier.html) to check dsigs. However, be aware that this validator is faulty. Namely:
158 |
159 | 1. The public key must be embedded into the XML markup.
160 | 2. Valid documents that are "pretty-printed" fail validation, but pass once the extra tabs/newlines are removed.
161 | 3. It only works with RSA encryption.
162 |
163 | ### Returning the document
164 |
165 | `XmlDsig\XmlDigitalSignature.getSignedDocument()` returns the canonicalized XML markup as a string.
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marcelxyz/php-xml-digital-signature",
3 | "description": "A PHP library for signing XML documents using digital signatures",
4 | "license": "MIT",
5 | "keywords": [
6 | "xml",
7 | "signature",
8 | "xmldsig",
9 | "xml-dsig",
10 | "xml-sig",
11 | "pkcs"
12 | ],
13 | "homepage": "https://github.com/marcelxyz/php-XmlDigitalSignature",
14 | "require": {
15 | "php": ">= 5.3"
16 | },
17 | "require-dev": {
18 | "phpunit/phpunit": "^6.1"
19 | },
20 | "authors": [
21 | {
22 | "name": "Marcel Tyszkiewicz",
23 | "email": "marcel.tyszkiewicz@gmail.com"
24 | }
25 | ],
26 | "suggest": {
27 | "ext-openssl": "OpenSSL extension",
28 | "ext-mcrypt": "MCrypt extension"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "XmlDsig\\": "src/"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/keys/private.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | Proc-Type: 4,ENCRYPTED
3 | DEK-Info: AES-256-CBC,A9C4912073685A58C92A6D7CCA5B9AE5
4 |
5 | b5yOOdgj5FDOcwwh310C+stf8RH2pNNifWXu0uYmYeSrSgrxlFoMa6d0FGJ2UK/s
6 | kvmBX3HvTQ+vfkDOTpHx5gXifc7gSUyZ8Tk0E9zJh81PR0U+cAp/dUsD2wMcqHqp
7 | sa/WQPPP86imZVVFhkx5tU+42VWOl2ZQuuyuRaC2hMqeTclQMDjAiXgkm1LL/V0g
8 | JbThgv4gtlh6D6Av5D2s3Thz+wmjFN2yTAoTjlwwNZaGyxNslfSB79JGrdn8+rk0
9 | uIJVRZQ9kZ3qNOEb+njebSBo5nS9VQvwzxig7DEyJQJX8KPnDbfFVpVZnrD6BXYk
10 | +T0s09ltWSIw2PiKVg10NrZPNmSGYS5kd1Uq/KJbELpGRUyaBLPQ66X91G1ksymW
11 | EwQ7cLVQ2LRiSaMYIIP67vHgIfp8nolJLg7d4oYQcyJfwVISWrVv4ddX0JlPz7kY
12 | 1P3fD/ikn8FcnLcCKUnJla0Z25tpyUYXFnoTfL9CBPtIDgwuLnD75WvDOKC+R2zB
13 | tStAmNw1dFFf4vfxiNyRTzyh0jmxkvif5ti0qOlXWmgk4g5rM3SAbKKKYwgau3XS
14 | QJsCHdsnFDGImBErVB054530HU2R8HNxj5xr1l7J+r53qh+GE7YLe8sPmAqMy2mB
15 | t8j78h1PcDwkNCNKUSrtnSYHHUrM5bJunPFKZoGv1CovXIHahpGiA/2NVUXt3u3i
16 | cO0kkBIqSRvkNaNzIgREQ2U+c8P12b9GuWXdTKrZnsofGUodk7Wf4a1/QJRQwU7O
17 | 8a8+HB5cY0MELRbAk2YrH9Zc23VwiAtTPK2gJXWu+S4POL2kQhXgZW73dxCGFH61
18 | TG1/qxnNcNyVpqmn7ejs5RdUKJ3fxxU0GkzAlNcER6AZ8ClS3jvNKNFPrCq4/WgK
19 | 6TB14tJJTvtd4ulZgjuzucdh0U9cDVv+I+3F6sm4Z89i0EKna9Ct0A+U2lTdlOQX
20 | X0wTNM6HQe8ExGJtiZZu8cvAwPoAPAEAlQZlYZSbL0Y3s7DhyT9uYf4vwItW2+qp
21 | tNPuQWLTMcnHxbo6/TumrFRDRdG/2pARqkZdb2nLyKBQbxD2PDOIuha9NLvbbDId
22 | CxjEH2CF3PxFX+DzwA/C7e6cm7vqZ85BMK6Pdporhiio8YLB/QPWWcj4rmxDp6pS
23 | meRtxwKXUAidmSrNPSHQxkyaOBhRlR6K3hr3KhN/RcnBUPjf3b409s9RsNRMhJyt
24 | 2+25hRrls7Xm6Fjf5/pCZpXjvpChSw4znrODyWLj9A+bJ68xEZkkwVeZXnzusbWI
25 | kuljX7PmqdlB4XB3HABdJTSnoS1pJo0/fCD0mO+o5/BFhcqcEqxdg993olM/b/1c
26 | yYaC1r1/R8tlx16B17/OEcg+8h0xfjA+3NiCPIQG4KUKI48u6F67uhL73sCszJvg
27 | BfYec7Bav6qqRHyLB8qwRqQ7qOAGPdmjm4TrCbmMGdEaSHj6WtPPcD7emkipubKd
28 | Lgl+H+gyCekluBlzwPx7iwI6gZOnOffwixHRDZkTKQfA0YQf6Oqw4EfDOkKGz55b
29 | lTkXk5o8CdHqPJNwI+wxY8B0l6w84rhDq6mudjWq7HJx0NAV3QpWIAiND9pIWe4v
30 | -----END RSA PRIVATE KEY-----
31 |
--------------------------------------------------------------------------------
/examples/keys/public.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwIwOlqlUOzhC0VRfNYZo
3 | HXQaS936fJKYmQGX+IzPGuK0eHKNDCUcKEpkpBc1K9U0FjO+vfGpodayd8fNWwFp
4 | Z+9E8t/ZD9253bN3xXsZtdthijmKLLi5vZ1fRVDFXc8fq2uZiBhgtr4F8vLVf6jn
5 | 3EBFH2PAeaz8A6YGnNs2KAGBGZ6pSMj4TNZqAuNLyTSwaumFkjPDOzBwW6c0FRg9
6 | 00Lk53Yq2jl020vclGOloHquhLPsK4V+Dt5SwrJ4FyIPm5RK1pmIjj8Kk1aWPlN5
7 | MYFRaWfC1c+faKWGcwJc4hdDIYJvPbL60uRib++vXirs5qf2MTytP6/+OhEs6F1o
8 | wQIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/examples/keys/public.xml:
--------------------------------------------------------------------------------
1 | wIwOlqlUOzhC0VRfNYZoHXQaS936fJKYmQGX+IzPGuK0eHKNDCUcKEpkpBc1K9U0FjO+vfGpodayd8fNWwFpZ+9E8t/ZD9253bN3xXsZtdthijmKLLi5vZ1fRVDFXc8fq2uZiBhgtr4F8vLVf6jn3EBFH2PAeaz8A6YGnNs2KAGBGZ6pSMj4TNZqAuNLyTSwaumFkjPDOzBwW6c0FRg900Lk53Yq2jl020vclGOloHquhLPsK4V+Dt5SwrJ4FyIPm5RK1pmIjj8Kk1aWPlN5MYFRaWfC1c+faKWGcwJc4hdDIYJvPbL60uRib++vXirs5qf2MTytP6/+OhEs6F1owQ==AQAB
--------------------------------------------------------------------------------
/examples/rsa_blob_example.php:
--------------------------------------------------------------------------------
1 | setCryptoAlgorithm(XmlDsig\XmlDigitalSignature::RSA_ALGORITHM)
9 | ->setDigestMethod(XmlDsig\XmlDigitalSignature::DIGEST_SHA512)
10 | ->forceStandalone();
11 |
12 | // load the private and public keys
13 | try
14 | {
15 | $dsig->loadPrivateKey(__DIR__ . '/keys/private.pem', 'MrMarchello');
16 | $dsig->loadPublicKey(__DIR__ . '/keys/public.pem');
17 | $dsig->loadPublicXmlKey(__DIR__ . '/keys/public.xml');
18 | }
19 | catch (\UnexpectedValueException $e)
20 | {
21 | print_r($e);
22 | exit(1);
23 | }
24 |
25 | try
26 | {
27 | $dsig->addObject('Lorem ipsum dolor sit amet');
28 | $dsig->sign();
29 | $dsig->verify();
30 | }
31 | catch (\UnexpectedValueException $e)
32 | {
33 | print_r($e);
34 | exit(1);
35 | }
36 |
37 | var_dump($dsig->getSignedDocument());
--------------------------------------------------------------------------------
/examples/rsa_dom_example.php:
--------------------------------------------------------------------------------
1 | setCryptoAlgorithm(XmlDsig\XmlDigitalSignature::RSA_ALGORITHM)
9 | ->setDigestMethod(XmlDsig\XmlDigitalSignature::DIGEST_SHA512)
10 | ->forceStandalone();
11 |
12 | // load the private and public keys
13 | try
14 | {
15 | $dsig->loadPrivateKey(__DIR__ . '/keys/private.pem', 'MrMarchello');
16 | $dsig->loadPublicKey(__DIR__ . '/keys/public.pem');
17 | $dsig->loadPublicXmlKey(__DIR__ . '/keys/public.xml');
18 | }
19 | catch (\UnexpectedValueException $e)
20 | {
21 | print_r($e);
22 | exit(1);
23 | }
24 |
25 | $fakeXml = new \DOMDocument();
26 | $fakeXml->loadXML('I am a happy camper');
27 |
28 | $node = $fakeXml->getElementsByTagName('baz')->item(0);
29 |
30 | try
31 | {
32 | $dsig->addObject($node, 'object', true);
33 | $dsig->sign();
34 | $dsig->verify();
35 | }
36 | catch (\UnexpectedValueException $e)
37 | {
38 | print_r($e);
39 | exit(1);
40 | }
41 |
42 | var_dump($dsig->getSignedDocument());
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./test/
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/XmlDigitalSignature.php:
--------------------------------------------------------------------------------
1 | node (allowing the receiving party to
101 | * properly verify the integrity of the received data).
102 | *
103 | * @see http://www.w3.org/TR/xmlsec-algorithms/#digest-method-uris
104 | * @var array
105 | */
106 | protected $digestMethodUriMapping = array(
107 | self::DIGEST_SHA1 => 'http://www.w3.org/2000/09/xmldsig#sha1',
108 | self::DIGEST_SHA256 => 'http://www.w3.org/2001/04/xmlenc#sha256',
109 | self::DIGEST_SHA512 => 'http://www.w3.org/2001/04/xmlenc#sha512',
110 | self::DIGEST_RIPEMD160 => 'http://www.w3.org/2001/04/xmlenc#ripemd160',
111 | );
112 |
113 | /**
114 | * Mapping of digest methods to their appropriate OpenSSL hashing algorithms.
115 | * These values must be compatible with the openssl_sign() and openssl_verify()
116 | * functions.
117 | *
118 | * @see http://www.php.net/manual/en/openssl.signature-algos.php
119 | * @var array
120 | */
121 | protected $openSSLAlgoMapping = array(
122 | self::DIGEST_SHA1 => OPENSSL_ALGO_SHA1,
123 | self::DIGEST_SHA256 => OPENSSL_ALGO_SHA256,
124 | self::DIGEST_SHA512 => OPENSSL_ALGO_SHA512,
125 | self::DIGEST_RIPEMD160 => OPENSSL_ALGO_RMD160,
126 | );
127 |
128 | /**
129 | * Mapping of key cryptography algorithms to their respective W3 spec URIs,
130 | * based on the selected digest method and crypto algorithm.
131 | *
132 | * @see http://www.w3.org/TR/xmlsec-algorithms/#signature-method-uris
133 | * @var array
134 | */
135 | protected $digestSignatureAlgoMapping = array(
136 | self::RSA_ALGORITHM => array(
137 | self::DIGEST_SHA1 => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
138 | self::DIGEST_SHA256 => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
139 | self::DIGEST_SHA512 => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512',
140 | self::DIGEST_RIPEMD160 => 'http://www.w3.org/2001/04/xmldsig-more#rsa-ripemd160',
141 | ),
142 | self::DSA_ALGORITHM => array(
143 | self::DIGEST_SHA1 => 'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
144 | self::DIGEST_SHA256 => 'http://www.w3.org/2009/xmldsig11#dsa-sha256',
145 | // DSA does not support SHA512 or RIPMED160
146 | // see http://tools.ietf.org/html/rfc5754#section-3.1 for more info
147 | ),
148 | self::ECDSA_ALGORITHM => array(
149 | self::DIGEST_SHA1 => 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1',
150 | self::DIGEST_SHA256 => 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256',
151 | self::DIGEST_SHA512 => 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384',
152 | self::DIGEST_RIPEMD160 => 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha51',
153 | ),
154 | self::HMAC_ALGORITHM => array(
155 | self::DIGEST_SHA1 => 'http://www.w3.org/2000/09/xmldsig#hmac-sha1',
156 | self::DIGEST_SHA256 => 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha256',
157 | self::DIGEST_SHA512 => 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha512',
158 | self::DIGEST_RIPEMD160 => 'http://www.w3.org/2001/04/xmldsig-more#hmac-ripemd160',
159 | ),
160 | );
161 |
162 | /**
163 | * Mapping of canonicalization attributes, based on the selected C14N method.
164 | * These must match those required by the DOMNode::C14N() method and the
165 | * W3 recommendations.
166 | *
167 | * @var array
168 | */
169 | protected $c14nOptionMapping = array(
170 | self::C14N => array('exclusive' => false, 'withComments' => false),
171 | self::C14N_COMMENTS => array('exclusive' => false, 'withComments' => true),
172 | self::C14N_EXCLUSIVE => array('exclusive' => true, 'withComments' => false),
173 | self::C14N_EXCLUSIVE_COMMENTS => array('exclusive' => true, 'withComments' => true),
174 | );
175 |
176 | /**
177 | * XML document to sign
178 | * @var DOMDocument
179 | */
180 | protected $doc;
181 |
182 | /**
183 | * OpenSSL handle to the private key used to sign the XML document
184 | * @var resource
185 | */
186 | protected $privateKey;
187 |
188 | /**
189 | * OpenSSL handle to the public key to include in the XML document
190 | * @var resource
191 | */
192 | protected $publicKey;
193 |
194 | /**
195 | * XML canonicalization method to use to canonicalize the document
196 | * @var string
197 | */
198 | protected $canonicalMethod = self::C14N;
199 |
200 | /**
201 | * Hashing algorithm to use for the digest
202 | * @var string
203 | */
204 | protected $digestMethod = self::DIGEST_SHA1;
205 |
206 | /**
207 | * Cryptography algorithm of the private key
208 | * @var int
209 | */
210 | protected $cryptoAlgorithm = self::RSA_ALGORITHM;
211 |
212 | /**
213 | * XML standalone declaration
214 | * @var bool
215 | */
216 | protected $standalone = false;
217 |
218 | /**
219 | * Namespace prefix for each node name
220 | * @var string
221 | */
222 | protected $nodeNsPrefix = 'dsig:';
223 |
224 | /**
225 | * Sets the cryptography algorithm used to generate the private key.
226 | *
227 | * @param int $algo Algorithm type (class const)
228 | * @return XmlDigitalSignature
229 | */
230 | public function setCryptoAlgorithm($algo)
231 | {
232 | if (!array_key_exists($algo, $this->digestSignatureAlgoMapping))
233 | {
234 | trigger_error('The chosen crypto algorithm does not appear to be predefined', E_USER_WARNING);
235 | }
236 | else if (!array_key_exists($this->digestMethod, $this->digestSignatureAlgoMapping[$algo]))
237 | {
238 | trigger_error('The chosen crypto algorithm does not support the chosen digest method', E_USER_WARNING);
239 | }
240 | else
241 | {
242 | $this->cryptoAlgorithm = $algo;
243 | }
244 |
245 | return $this;
246 | }
247 |
248 | /**
249 | * Sets the namespace prefix for each generated node name.
250 | * For example, to create an XML tree with node names of
251 | * type , simply pass the value 'foo' to this method.
252 | *
253 | * @param string $prefix The namespace prefix
254 | * @return XmlDigitalSignature
255 | */
256 | public function setNodeNsPrefix($prefix)
257 | {
258 | if (is_string($prefix) && strlen($prefix))
259 | {
260 | $this->nodeNsPrefix = rtrim($prefix, ':') . ':';
261 | }
262 | else
263 | {
264 | $this->nodeNsPrefix = '';
265 | }
266 |
267 | return $this;
268 | }
269 |
270 | /**
271 | * Forces the signed XML document to be standalone
272 | *
273 | * @return XmlDigitalSignature
274 | */
275 | public function forceStandalone()
276 | {
277 | $this->standalone = true;
278 | return $this;
279 | }
280 |
281 | /**
282 | * Sets the canonical method used to canonicalize the document
283 | *
284 | * @param string $method Canonicalization method (class const)
285 | * @return XmlDigitalSignature
286 | */
287 | public function setCanonicalMethod($method)
288 | {
289 | if (array_key_exists($method, $this->c14nOptionMapping))
290 | {
291 | $this->canonicalMethod = $method;
292 | }
293 | else
294 | {
295 | trigger_error(sprintf('The chosen canonical method (%s) is not supported', $method), E_USER_WARNING);
296 | }
297 |
298 | return $this;
299 | }
300 |
301 | /**
302 | * Sets the digest method (hashing algo) used to calculate the digest of the document
303 | *
304 | * @param string $method Digest method (class const)
305 | * @return XmlDigitalSignature
306 | */
307 | public function setDigestMethod($method)
308 | {
309 | if (array_key_exists($method, $this->openSSLAlgoMapping) &&
310 | array_key_exists($method, $this->digestMethodUriMapping))
311 | {
312 | $this->digestMethod = $method;
313 | }
314 | else
315 | {
316 | trigger_error(sprintf('The chosen digest method (%s) is not supported', $method), E_USER_WARNING);
317 | }
318 |
319 | $this->checkDigestSupport();
320 |
321 | return $this;
322 | }
323 |
324 | /**
325 | * Returns the signed XML document
326 | *
327 | * @return string Signed XML document
328 | */
329 | public function getSignedDocument()
330 | {
331 | return $this->doc->saveXML();
332 | }
333 |
334 | /**
335 | * Loads a PEM formatted private key.
336 | *
337 | * @param string $key The private key in PEM format or a path to the key (see openssl_pkey_get_private)
338 | * @param string $passphrase Password to the key file (if there is one)
339 | * @param bool $isFile Whether the key is a path to a file
340 | * @return bool True if the key was successfully loaded, false otherwise
341 | * @throws \UnexpectedValueException Thrown if the key cannot be loaded
342 | */
343 | public function loadPrivateKey($key, $passphrase = null, $isFile = true)
344 | {
345 | return $this->loadKey($key, $isFile, true, $passphrase);
346 | }
347 |
348 | /**
349 | * Loads a public key, either an X.509 cert or PEM formatted key
350 | *
351 | * @param mixed $key X.509 cert resource, path to the key, or the key (see openssl_pkey_get_public)
352 | * @throws \UnexpectedValueException Thrown if the key cannot be loaded
353 | * @return bool True if the key was successfully loaded, false otherwise
354 | */
355 | public function loadPublicKey($key, $isFile = true)
356 | {
357 | return $this->loadKey($key, $isFile);
358 | }
359 |
360 | /**
361 | * Loads a public/private key into memory.
362 | *
363 | * @param string $key Either the path to the key, or the key as a string
364 | * @param bool $isFile Whether the first arg is a path that needs to be opened
365 | * @param bool $isPrivate Whether the key is private
366 | * @param string $passphrase If the key is private and has a passphrase, this is the place to give it
367 | * @throws \UnexpectedValueException Thrown if the key cannot be read, or if OpenSSL does not like it
368 | * @return bool True if the key is successfully loaded, false otherwise
369 | */
370 | protected function loadKey($key, $isFile, $isPrivate = false, $passphrase = null)
371 | {
372 | // load the key from the file, if that's what they say
373 | if (true === $isFile)
374 | {
375 | try
376 | {
377 | $key = $this->loadFile($key);
378 | }
379 | catch (\UnexpectedValueException $e)
380 | {
381 | // up, up and away!
382 | throw $e;
383 | }
384 | }
385 |
386 | // handle the key based on whether it's public or private
387 | if (true === $isPrivate)
388 | {
389 | $privKey = openssl_pkey_get_private($key, $passphrase);
390 |
391 | if (false === $privKey)
392 | {
393 | throw new \UnexpectedValueException('Unable to load the private key');
394 | }
395 |
396 | $this->privateKey = $privKey;
397 | }
398 | // good ol' public key
399 | else
400 | {
401 | $pubKey = openssl_pkey_get_public($key);
402 |
403 | if (false === $pubKey)
404 | {
405 | throw new \UnexpectedValueException('Unable to load the public key');
406 | }
407 |
408 | $this->publicKey = $pubKey;
409 | }
410 |
411 | return true;
412 | }
413 |
414 | /**
415 | * Loads a key from a specified file location.
416 | *
417 | * @param string $filePath Location of the key to be loaded
418 | * @throws \UnexpectedValueException Thrown if the file cannot be loaded or is empty
419 | * @return string|bool False on failure, the key as a string otherwise
420 | */
421 | protected function loadFile($filePath)
422 | {
423 | if (!file_exists($filePath) || !is_readable($filePath))
424 | {
425 | throw new \UnexpectedValueException(sprintf('Unable to open the "%s" file', $filePath));
426 | }
427 |
428 | $key = @file_get_contents($filePath);
429 |
430 | if (!is_string($key) || 0 === strlen($key))
431 | {
432 | throw new \UnexpectedValueException(sprintf('File "%s" appears to be empty', $filePath));
433 | }
434 |
435 | return $key;
436 | }
437 |
438 | /**
439 | * Loads a public key in an XML format.
440 | *
441 | * The first argument provided to this function can be a path to the key (the second arg must be set to true).
442 | * Otherwise, you may pass the actual XML string as the first argument (set the second argument to false).
443 | * The third argument is needed to create a reference between the created element and its .
444 | *
445 | * @param DOMDocument|string $publicKey The DOMDocument containing the key, or a path to the key's location, or the key as a string
446 | * @param string $isFile If set to true, the key will be loaded from the given path
447 | * @param string $objectId ID attribute of the key (used to create a reference between the key and its node)
448 | * @throws \UnexpectedValueException Thrown when the provided key is in an unsupported format
449 | * @return bool True if the key was successfully loaded, false otherwise
450 | */
451 | public function loadPublicXmlKey($publicKey, $isFile = true, $objectId = null)
452 | {
453 | if (true === $isFile)
454 | {
455 | try
456 | {
457 | $publicKey = $this->loadFile($publicKey);
458 | }
459 | catch (\UnexpectedValueException $e)
460 | {
461 | throw $e;
462 | }
463 | }
464 |
465 | $keyNode = null;
466 |
467 | // if the key is a string, assume that it's valid XML markup and load it into a dom docuemnt
468 | if (is_string($publicKey) && strlen($publicKey))
469 | {
470 | $keyNode = new \DOMDocument;
471 |
472 | if (!@$keyNode->loadXML($publicKey))
473 | {
474 | throw new \UnexpectedValueException('The provided public XML key does not appear to be well structured XML');
475 | }
476 | }
477 | // DOM nodes are sexy as fuck
478 | else if (is_object($publicKey) && $publicKey instanceof DOMDocument)
479 | {
480 | $keyNode = $publicKey;
481 | }
482 | // woops, a bad key was provided :(
483 | else
484 | {
485 | throw new \UnexpectedValueException('Unsupported XML public key provided');
486 | }
487 |
488 | // add the key to the DOM
489 | return $this->appendXmlPublicKey($keyNode, $objectId);
490 | }
491 |
492 | /**
493 | * Appends the public XML key to the DOM document.
494 | *
495 | * @param \DOMDocument $keyDoc The DOM document containing the public key information
496 | * @param string $objectId ID attribute of the key
497 | * @throws \UnexpectedValueException If the XML tree is not intact
498 | * @return bool True if the key was successfully appended, false otherwise
499 | */
500 | protected function appendXmlPublicKey(\DOMDocument $keyDoc, $objectId)
501 | {
502 | // create the document structure if necessary
503 | if (is_null($this->doc))
504 | {
505 | $this->createXmlStructure();
506 | }
507 |
508 | // local the node to which the key will be appended
509 | $keyValue = $this->doc->getElementsByTagName($this->nodeNsPrefix . 'KeyValue')->item(0);
510 | if (is_null($keyValue))
511 | {
512 | throw new \UnexpectedValueException('Unabled to locate the KeyValue node');
513 | }
514 |
515 | // we have to add the proper namespace prefixes to all of the nodes in the public key DOM
516 | $publicKeyNode = $this->doc->createElement($this->nodeNsPrefix . $keyDoc->firstChild->nodeName);
517 | $keyValue->appendChild($publicKeyNode);
518 |
519 | foreach ($keyDoc->firstChild->childNodes as $node)
520 | {
521 | $newNode = $this->doc->createElement($this->nodeNsPrefix . $node->nodeName, $node->nodeValue);
522 | $publicKeyNode->appendChild($newNode);
523 | }
524 |
525 | // add the id attribute, if its provided
526 | if (is_string($objectId) && strlen($objectId))
527 | {
528 | $keyValue->parentNode->setAttribute('Id', $objectId);
529 | }
530 |
531 | return true;
532 | }
533 |
534 | /**
535 | * Appends a reference to the XML document of the provided node,
536 | * by canonicalizing it first and then digesting (hashing) it.
537 | * The actual digest is appended to the DOM.
538 | *
539 | * @param \DOMNode $node The node that is to be referenced
540 | * @param string $uri Reference URI attribute
541 | * @return bool
542 | */
543 | public function addReference(\DOMNode $node, $uri = null)
544 | {
545 | if (is_null($this->doc))
546 | {
547 | $this->createXmlStructure();
548 | }
549 |
550 | // references are appended to the SignedInfo node
551 | $signedInfo = $this->doc->getElementsByTagName($this->nodeNsPrefix . 'SignedInfo')->item(0);
552 |
553 | $reference = $this->doc->createElement($this->nodeNsPrefix . 'Reference');
554 | $signedInfo->appendChild($reference);
555 |
556 | if (is_string($uri) && strlen($uri))
557 | {
558 | // if the URI is a simple string (i.e. it's an ID that's a reference to an object in the DOM)
559 | // prepend it with a hash
560 | // otherwise (if the uri is URL-like), do nothing
561 | if (!filter_var($uri, FILTER_VALIDATE_URL))
562 | {
563 | $uri = '#' . $uri;
564 | }
565 |
566 | $reference->setAttribute('URI', $uri);
567 | }
568 |
569 | // specify the digest (hashing) algorithm used
570 | $digestMethod = $this->doc->createElement($this->nodeNsPrefix . 'DigestMethod');
571 | $digestMethod->setAttribute('Algorithm', $this->digestMethodUriMapping[$this->digestMethod]);
572 | $reference->appendChild($digestMethod);
573 |
574 | // first we must try to canonicalize the element(s)
575 | try
576 | {
577 | $c14nData = $this->canonicalize($node);
578 | }
579 | catch (\UnexpectedValueException $e)
580 | {
581 | throw $e;
582 | }
583 |
584 | // references are stored as digests, so we must do that as well
585 | $referenceDigest = $this->calculateDigest($c14nData);
586 |
587 | $digestValue = $this->doc->createElement($this->nodeNsPrefix . 'DigestValue', $referenceDigest);
588 | $reference->appendChild($digestValue);
589 |
590 | return true;
591 | }
592 |
593 | /**
594 | * Signs the XML document with an XML digital signature
595 | *
596 | * @throws \UnexpectedValueException If the XML tree is not intact or if there is no OpenSSL mapping set
597 | * @return bool True if the document was successfully signed, false otherwise
598 | */
599 | public function sign()
600 | {
601 | // the document must be set up
602 | if (is_null($this->doc))
603 | {
604 | return new \UnexpectedValueException('No document structure to sign');
605 | }
606 |
607 | // find the SignedInfo element, which is what we will actually sign
608 | $signedInfo = $this->doc->getElementsByTagName($this->nodeNsPrefix . 'SignedInfo')->item(0);
609 | if (is_null($signedInfo))
610 | {
611 | throw new \UnexpectedValueException('Unabled to locate the SignedInfo node');
612 | }
613 |
614 | // canonicalize the SignedInfo element for signing
615 | $c14nSignedInfo = $this->canonicalize($signedInfo);
616 |
617 | // make sure that we know which OpenSSL algo type to use
618 | if (!array_key_exists($this->digestMethod, $this->openSSLAlgoMapping))
619 | {
620 | throw new \UnexpectedValueException('No OpenSSL algorithm has been defined for digest of type ' . $this->digestMethod);
621 | }
622 |
623 | // sign the SignedInfo element using the private key
624 | if (!openssl_sign($c14nSignedInfo, $signature, $this->privateKey, $this->openSSLAlgoMapping[$this->digestMethod]))
625 | {
626 | throw new \UnexpectedValueException('Unable to sign the document. Error: ' . openssl_error_string());
627 | }
628 |
629 | $signature = base64_encode($signature);
630 |
631 | // find the signature value node, to which we will append the base64 encoded signature
632 | $signatureNode = $this->doc->getElementsByTagName($this->nodeNsPrefix . 'SignatureValue')->item(0);
633 | if (is_null($signatureNode))
634 | {
635 | throw new \UnexpectedValueException('Unabled to locate the SingatureValue node');
636 | }
637 |
638 | $signatureNode->appendChild($this->doc->createTextNode($signature));
639 |
640 | return true;
641 | }
642 |
643 | /**
644 | * Verifies the XML digital signature
645 | *
646 | * @throws \UnexpectedValueException If the XML tree is not intact
647 | * @return bool Verification result
648 | */
649 | public function verify()
650 | {
651 | if (is_null($this->publicKey))
652 | {
653 | trigger_error('Cannot verify XML digital signature without public key', E_USER_WARNING);
654 | return false;
655 | }
656 |
657 | // find the SignedInfo element which was signed
658 | $signedInfo = $this->doc->getElementsByTagName($this->nodeNsPrefix . 'SignedInfo')->item(0);
659 | if (is_null($signedInfo))
660 | {
661 | throw new \UnexpectedValueException('Unable to locate the SignedInfo node');
662 | }
663 |
664 | // canonicalize the SignedInfo element for signature checking
665 | $c14nSignedInfo = $this->canonicalize($signedInfo);
666 |
667 | // find the signature value to verify
668 | $signatureValue = $this->doc->getElementsByTagName($this->nodeNsPrefix . 'SignatureValue')->item(0);
669 | if (is_null($signatureValue))
670 | {
671 | throw new \UnexpectedValueException('Unable to locate the SignatureValue node');
672 | }
673 |
674 | $signature = base64_decode($signatureValue->nodeValue);
675 |
676 | return 1 === openssl_verify($c14nSignedInfo, $signature, $this->publicKey, $this->openSSLAlgoMapping[$this->digestMethod]);
677 | }
678 |
679 | /**
680 | * Prepares the XML skeleton structure for the signature
681 | *
682 | * return void
683 | */
684 | protected function createXmlStructure()
685 | {
686 | $this->doc = new \DOMDocument('1.0', 'UTF-8');
687 | $this->doc->xmlStandalone = $this->standalone;
688 |
689 | // Signature node
690 | $signature = $this->doc->createElementNS(self::XML_DSIG_NS, $this->nodeNsPrefix . 'Signature');
691 | $this->doc->appendChild($signature);
692 |
693 | // SignedInfo node
694 | $signedInfo = $this->doc->createElement($this->nodeNsPrefix . 'SignedInfo');
695 | $signature->appendChild($signedInfo);
696 |
697 | // canonicalization method node
698 | $c14nMethod = $this->doc->createElement($this->nodeNsPrefix . 'CanonicalizationMethod');
699 | $c14nMethod->setAttribute('Algorithm', $this->canonicalMethod);
700 | $signedInfo->appendChild($c14nMethod);
701 |
702 | // specify the hash algorithm used
703 | $sigMethod = $this->doc->createElement($this->nodeNsPrefix . 'SignatureMethod');
704 | $sigMethod->setAttribute('Algorithm', $this->chooseSignatureMethod());
705 | $signedInfo->appendChild($sigMethod);
706 |
707 | // create the node that will hold the signature
708 | $sigValue = $this->doc->createElement($this->nodeNsPrefix . 'SignatureValue');
709 | $signature->appendChild($sigValue);
710 |
711 | // the KeyInfo and KeyValue nodes will contain information about the public key
712 | $keyInfo = $this->doc->createElement($this->nodeNsPrefix . 'KeyInfo');
713 | $signature->appendChild($keyInfo);
714 |
715 | $keyValue = $this->doc->createElement($this->nodeNsPrefix . 'KeyValue');
716 | $keyInfo->appendChild($keyValue);
717 | }
718 |
719 | /**
720 | * Chooses the appropriate W3 signature URI, based on
721 | * the chosen crypto algorithm and digest method.
722 | *
723 | * @return string Signature method URI
724 | */
725 | protected function chooseSignatureMethod()
726 | {
727 | return $this->digestSignatureAlgoMapping[$this->cryptoAlgorithm][$this->digestMethod];
728 | }
729 |
730 | /**
731 | * Canonicalizes a DOM document or a single DOM node
732 | *
733 | * @param \DOMNode $data Node(s) to be canonicalized
734 | * @throws \UnexpectedValueException If the canonicalization process failed
735 | * @return string|bool Canonicalized node(s), or false on failure
736 | */
737 | protected function canonicalize(\DOMNode $object)
738 | {
739 | $options = $this->c14nOptionMapping[$this->canonicalMethod];
740 |
741 | // canonicalize the provided data with the preset options
742 | $c14nData = $object->C14N($options['exclusive'], $options['withComments']);
743 |
744 | if (is_string($c14nData) && strlen($c14nData))
745 | {
746 | return $c14nData;
747 | }
748 |
749 | throw new \UnexpectedValueException('Unable to canonicalize the provided DOM document');
750 | }
751 |
752 | /**
753 | * Appends an object to the signed XML documents
754 | *
755 | * @param DOMNode|string $data Data to add to the object node
756 | * @param string $objectId ID attribute of the object
757 | * @param bool $digestObject Whether the object data should be digested
758 | * @throws \UnexpectedValueException If the canonicalization process failed
759 | */
760 | public function addObject($data, $objectId = null, $digestObject = false)
761 | {
762 | if (is_null($this->doc))
763 | {
764 | $this->createXmlStructure();
765 | }
766 |
767 | if (is_string($data) && strlen($data))
768 | {
769 | $data = $this->doc->createTextNode($data);
770 | }
771 | else if (!is_object($data) || !$data instanceof \DOMNode)
772 | {
773 | throw new \UnexpectedValueException(sprintf('Digested data must be a non-empty string or DOMNode, %s was given', gettype($data)));
774 | }
775 |
776 | // if the object is meant to be digested, do so
777 | if (true === $digestObject)
778 | {
779 | $digestedData = $this->calculateDigest($this->canonicalize($data));
780 | $data = $this->doc->createTextNode($digestedData);
781 | }
782 | else
783 | {
784 | $data = $this->doc->importNode($data, true);
785 | }
786 |
787 | // add the object to the dom
788 | $object = $this->doc->createElement($this->nodeNsPrefix . 'Object');
789 | $object->appendChild($data);
790 | $this->doc->getElementsByTagName('Signature')->item(0)->appendchild($object);
791 |
792 | // objects must have an id attribute which will
793 | // correspond to the reference URI attribute
794 | if (!is_string($objectId) || !strlen($objectId) || is_numeric($objectId[0]))
795 | {
796 | // generate a random ID
797 | $objectId = rtrim(base64_encode(mt_rand()), '=');
798 | }
799 |
800 | // if the ID was provided, add it
801 | $object->setAttribute('Id', $objectId);
802 |
803 | // objects also need to be digested and stored as references
804 | // so that they can be signed later
805 | $this->addReference($object, $objectId);
806 |
807 | return true;
808 | }
809 |
810 | /**
811 | * Calculates the digest (hash) of a given input value, based on the chosen hashing algorithm.
812 | *
813 | * @param string $data Data to the hashed
814 | * @return string Digested string encoded in base64
815 | */
816 | protected function calculateDigest($data)
817 | {
818 | $this->checkDigestSupport();
819 |
820 | return base64_encode(hash($this->digestMethod, $data, true));
821 | }
822 |
823 | /**
824 | * Ensures that the current installation of PHP supports the selected digest method.
825 | * If it does not, a fatal error is triggered.
826 | *
827 | * @return void
828 | */
829 | protected function checkDigestSupport()
830 | {
831 | // ensure that the selected digest method is supported by the current PHP version
832 | if (!in_array($this->digestMethod, hash_algos()))
833 | {
834 | trigger_error(sprintf('This installation of PHP does not support the %s hashing algorithm', $this->digestMethod), E_USER_ERROR);
835 | }
836 | }
837 | }
838 |
--------------------------------------------------------------------------------
/test/XmlDigitalSignatureTest.php:
--------------------------------------------------------------------------------
1 | dsig = new XmlDigitalSignature();
29 | }
30 |
31 | /**
32 | * Test whether a malformed reference causes an exception.
33 | */
34 | public function testAddBadReference() {
35 | $this->expectException(\UnexpectedValueException::class);
36 | $node = $this
37 | ->getMockBuilder(\DOMNode::class)
38 | ->setMethods(['C14N'])
39 | ->getMock();
40 | $this->dsig->addReference($node);
41 | }
42 |
43 | /**
44 | * Test whether keys load successfully.
45 | */
46 | public function testLoadKeys() {
47 | $result = $this->dsig->loadPrivateKey(self::PRIVATE_KEY, self::PRIVATE_KEY_PASSPHRASE);
48 | $this->assertTrue($result);
49 |
50 | $result = $this->dsig->loadPrivateKey(file_get_contents(self::PRIVATE_KEY), self::PRIVATE_KEY_PASSPHRASE, false);
51 | $this->assertTrue($result);
52 |
53 | $result = $this->dsig->loadPublicKey(self::PUBLIC_KEY);
54 | $this->assertTrue($result);
55 |
56 | $result = $this->dsig->loadPublicKey(file_get_contents(self::PUBLIC_KEY), false);
57 | $this->assertTrue($result);
58 |
59 | $result = $this->dsig->loadPublicXmlKey(file_get_contents(self::PUBLIC_XML_KEY), false);
60 | $this->assertTrue($result);
61 |
62 | $result = $this->dsig->loadPublicXmlKey(self::PUBLIC_XML_KEY);
63 | $this->assertTrue($result);
64 | }
65 |
66 | /**
67 | * Test whether a malformed private key causes an exception.
68 | */
69 | public function testBadPrivateKey() {
70 | $this->expectException(\UnexpectedValueException::class);
71 | $this->dsig->loadPrivateKey('abc', null, false);
72 | }
73 |
74 | /**
75 | * Test whether a nonexistent private key path causes an exception.
76 | */
77 | public function testBadPrivateKeyPath() {
78 | $this->expectException(\UnexpectedValueException::class);
79 | $this->dsig->loadPrivateKey(self::PRIVATE_KEY . 'a');
80 | }
81 |
82 | /**
83 | * Test whether an incorrect private key passphrase causes an exception.
84 | */
85 | public function testBadPrivateKeyPassphrase() {
86 | $this->expectException(\UnexpectedValueException::class);
87 | $this->dsig->loadPrivateKey(self::PRIVATE_KEY, self::PRIVATE_KEY_PASSPHRASE . 'a');
88 | }
89 |
90 | /**
91 | * Test whether a malformed public key causes an exception.
92 | */
93 | public function testBadPublicKey() {
94 | $this->expectException(\UnexpectedValueException::class);
95 | $this->dsig->loadPublicKey('abc', false);
96 | }
97 |
98 | /**
99 | * Test whether a nonexistent public key path causes an exception.
100 | */
101 | public function testBadPublicKeyPath() {
102 | $this->expectException(\UnexpectedValueException::class);
103 | $this->dsig->loadPublicKey(self::PUBLIC_KEY . 'a');
104 | }
105 |
106 | /**
107 | * Test whether a malformed public XML key causes an exception.
108 | */
109 | public function testBadPublicXmlKey() {
110 | $this->expectException(\UnexpectedValueException::class);
111 | $this->dsig->loadPublicXmlKey('abc', false);
112 | }
113 |
114 | /**
115 | * Test whether loading from a nonexistent public XML key path causes an exception.
116 | */
117 | public function testBadPublicXmlKeyPath() {
118 | $this->expectException(\UnexpectedValueException::class);
119 | $this->dsig->loadPublicXmlKey(self::PUBLIC_XML_KEY . 'a');
120 | }
121 |
122 | /**
123 | * Test whether setting a malformed crypto algorithm causes an exception.
124 | */
125 | public function testSetBadCryptoAlgorithm() {
126 | $this->expectException(Warning::class);
127 | $this->dsig->setCryptoAlgorithm(-1);
128 | }
129 |
130 | /**
131 | * Test whether setting a malformed canonical method causes an exception.
132 | */
133 | public function testSetBadCanonicalMethod() {
134 | $this->expectException(Warning::class);
135 | $this->dsig->setCanonicalMethod(XmlDigitalSignature::C14N_EXCLUSIVE . 'a');
136 | }
137 |
138 | /**
139 | * Test whether setting a malformed digest method causes an exception.
140 | */
141 | public function testSetBadDigestMethod() {
142 | $this->expectException(Warning::class);
143 | $this->dsig->setDigestMethod(XmlDigitalSignature::DIGEST_SHA512 . 'a');
144 | }
145 |
146 | /**
147 | * Test whether a valid object is successfully added.
148 | */
149 | public function testAddObject() {
150 | $result = $this->dsig->addObject('a');
151 | $this->assertTrue($result);
152 | }
153 |
154 | /**
155 | * Test whether adding an empty object causes an exception.
156 | */
157 | public function testAddEmptyObjectString() {
158 | $this->expectException(\UnexpectedValueException::class);
159 | $this->dsig->addObject('');
160 | }
161 |
162 | /**
163 | * Test whether adding a malformed object causes an exception.
164 | */
165 | public function testAddBadObject() {
166 | $this->expectException(\UnexpectedValueException::class);
167 | $this->dsig->addObject(new \stdClass());
168 | }
169 |
170 | /**
171 | * Test whether a valid reference is successfully added.
172 | */
173 | public function testAddReference() {
174 | $result = $this->dsig->addReference(Xml::load(''));
175 | $this->assertTrue($result);
176 | }
177 |
178 | /**
179 | * Test the whole signing process, with all the options.
180 | */
181 | public function testSigningProcess() {
182 | $this->dsig->setCanonicalMethod(XmlDigitalSignature::C14N_EXCLUSIVE_COMMENTS);
183 | $this->dsig->setCryptoAlgorithm(XmlDigitalSignature::HMAC_ALGORITHM);
184 | $this->dsig->setDigestMethod(XmlDigitalSignature::DIGEST_SHA256);
185 | $this->dsig->setNodeNsPrefix('xyz');
186 |
187 | $result = $this->dsig->loadPrivateKey(self::PRIVATE_KEY, self::PRIVATE_KEY_PASSPHRASE);
188 | $this->assertTrue($result);
189 |
190 | $result = $this->dsig->loadPublicKey(self::PUBLIC_KEY);
191 | $this->assertTrue($result);
192 |
193 | $result = $this->dsig->loadPublicXmlKey(self::PUBLIC_XML_KEY);
194 | $this->assertTrue($result);
195 |
196 | $result = $this->dsig->addObject('a', 'objectA');
197 | $this->assertTrue($result);
198 |
199 | $result = $this->dsig->sign();
200 | $this->assertTrue($result);
201 |
202 | $result = $this->dsig->verify();
203 | $this->assertTrue($result);
204 |
205 | $this->assertEquals(
206 | Xml::loadFile(self::DATA_DIR . 'expected-signed.xml')->saveXML(),
207 | $this->dsig->getSignedDocument()
208 | );
209 | }
210 | }
--------------------------------------------------------------------------------
/test/data/expected-signed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | jRh+bedAzKIBW7V7F0mPFiyI1xc+MMP7ebTPhKKHcUE=
9 |
10 |
11 | UDgylbMhH/BcF1et06Urmf0cSuCdIaUCLAwJ7o8328Bd4NcswBXi5/M0YQzZepKcfghD9gCdkvb0RVJ3J1F5aaj2Aus3FH6fnfUg9aRodOOOcs3wA+ziODrzljRuqIwqZSskKMwIf/0RE1JUWj/e0T6Z+HduGOyq7Xu7TclbkiZBTxwNE706wxT68khXX7S7iCeqXBTIy8MLAMgkZHxLjtslVcornJCuWx9vL3RL0Jsou7xabQojL99MGBtKNbT3O/4t+7ojxXvnT62YL4qU4KCWGxTJxjXj8y7xZrIu+2MDpWtlLtS2R26+EHNw5ZdW8cCipzbR0Cg/2vSgc9UBjA==
12 |
13 |
14 |
15 | wIwOlqlUOzhC0VRfNYZoHXQaS936fJKYmQGX+IzPGuK0eHKNDCUcKEpkpBc1K9U0FjO+vfGpodayd8fNWwFpZ+9E8t/ZD9253bN3xXsZtdthijmKLLi5vZ1fRVDFXc8fq2uZiBhgtr4F8vLVf6jn3EBFH2PAeaz8A6YGnNs2KAGBGZ6pSMj4TNZqAuNLyTSwaumFkjPDOzBwW6c0FRg900Lk53Yq2jl020vclGOloHquhLPsK4V+Dt5SwrJ4FyIPm5RK1pmIjj8Kk1aWPlN5MYFRaWfC1c+faKWGcwJc4hdDIYJvPbL60uRib++vXirs5qf2MTytP6/+OhEs6F1owQ==
16 | AQAB
17 |
18 |
19 |
20 | a
21 |
22 |
--------------------------------------------------------------------------------
/test/data/keys/private.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | Proc-Type: 4,ENCRYPTED
3 | DEK-Info: AES-256-CBC,A9C4912073685A58C92A6D7CCA5B9AE5
4 |
5 | b5yOOdgj5FDOcwwh310C+stf8RH2pNNifWXu0uYmYeSrSgrxlFoMa6d0FGJ2UK/s
6 | kvmBX3HvTQ+vfkDOTpHx5gXifc7gSUyZ8Tk0E9zJh81PR0U+cAp/dUsD2wMcqHqp
7 | sa/WQPPP86imZVVFhkx5tU+42VWOl2ZQuuyuRaC2hMqeTclQMDjAiXgkm1LL/V0g
8 | JbThgv4gtlh6D6Av5D2s3Thz+wmjFN2yTAoTjlwwNZaGyxNslfSB79JGrdn8+rk0
9 | uIJVRZQ9kZ3qNOEb+njebSBo5nS9VQvwzxig7DEyJQJX8KPnDbfFVpVZnrD6BXYk
10 | +T0s09ltWSIw2PiKVg10NrZPNmSGYS5kd1Uq/KJbELpGRUyaBLPQ66X91G1ksymW
11 | EwQ7cLVQ2LRiSaMYIIP67vHgIfp8nolJLg7d4oYQcyJfwVISWrVv4ddX0JlPz7kY
12 | 1P3fD/ikn8FcnLcCKUnJla0Z25tpyUYXFnoTfL9CBPtIDgwuLnD75WvDOKC+R2zB
13 | tStAmNw1dFFf4vfxiNyRTzyh0jmxkvif5ti0qOlXWmgk4g5rM3SAbKKKYwgau3XS
14 | QJsCHdsnFDGImBErVB054530HU2R8HNxj5xr1l7J+r53qh+GE7YLe8sPmAqMy2mB
15 | t8j78h1PcDwkNCNKUSrtnSYHHUrM5bJunPFKZoGv1CovXIHahpGiA/2NVUXt3u3i
16 | cO0kkBIqSRvkNaNzIgREQ2U+c8P12b9GuWXdTKrZnsofGUodk7Wf4a1/QJRQwU7O
17 | 8a8+HB5cY0MELRbAk2YrH9Zc23VwiAtTPK2gJXWu+S4POL2kQhXgZW73dxCGFH61
18 | TG1/qxnNcNyVpqmn7ejs5RdUKJ3fxxU0GkzAlNcER6AZ8ClS3jvNKNFPrCq4/WgK
19 | 6TB14tJJTvtd4ulZgjuzucdh0U9cDVv+I+3F6sm4Z89i0EKna9Ct0A+U2lTdlOQX
20 | X0wTNM6HQe8ExGJtiZZu8cvAwPoAPAEAlQZlYZSbL0Y3s7DhyT9uYf4vwItW2+qp
21 | tNPuQWLTMcnHxbo6/TumrFRDRdG/2pARqkZdb2nLyKBQbxD2PDOIuha9NLvbbDId
22 | CxjEH2CF3PxFX+DzwA/C7e6cm7vqZ85BMK6Pdporhiio8YLB/QPWWcj4rmxDp6pS
23 | meRtxwKXUAidmSrNPSHQxkyaOBhRlR6K3hr3KhN/RcnBUPjf3b409s9RsNRMhJyt
24 | 2+25hRrls7Xm6Fjf5/pCZpXjvpChSw4znrODyWLj9A+bJ68xEZkkwVeZXnzusbWI
25 | kuljX7PmqdlB4XB3HABdJTSnoS1pJo0/fCD0mO+o5/BFhcqcEqxdg993olM/b/1c
26 | yYaC1r1/R8tlx16B17/OEcg+8h0xfjA+3NiCPIQG4KUKI48u6F67uhL73sCszJvg
27 | BfYec7Bav6qqRHyLB8qwRqQ7qOAGPdmjm4TrCbmMGdEaSHj6WtPPcD7emkipubKd
28 | Lgl+H+gyCekluBlzwPx7iwI6gZOnOffwixHRDZkTKQfA0YQf6Oqw4EfDOkKGz55b
29 | lTkXk5o8CdHqPJNwI+wxY8B0l6w84rhDq6mudjWq7HJx0NAV3QpWIAiND9pIWe4v
30 | -----END RSA PRIVATE KEY-----
31 |
--------------------------------------------------------------------------------
/test/data/keys/public.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwIwOlqlUOzhC0VRfNYZo
3 | HXQaS936fJKYmQGX+IzPGuK0eHKNDCUcKEpkpBc1K9U0FjO+vfGpodayd8fNWwFp
4 | Z+9E8t/ZD9253bN3xXsZtdthijmKLLi5vZ1fRVDFXc8fq2uZiBhgtr4F8vLVf6jn
5 | 3EBFH2PAeaz8A6YGnNs2KAGBGZ6pSMj4TNZqAuNLyTSwaumFkjPDOzBwW6c0FRg9
6 | 00Lk53Yq2jl020vclGOloHquhLPsK4V+Dt5SwrJ4FyIPm5RK1pmIjj8Kk1aWPlN5
7 | MYFRaWfC1c+faKWGcwJc4hdDIYJvPbL60uRib++vXirs5qf2MTytP6/+OhEs6F1o
8 | wQIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/test/data/keys/public.xml:
--------------------------------------------------------------------------------
1 | wIwOlqlUOzhC0VRfNYZoHXQaS936fJKYmQGX+IzPGuK0eHKNDCUcKEpkpBc1K9U0FjO+vfGpodayd8fNWwFpZ+9E8t/ZD9253bN3xXsZtdthijmKLLi5vZ1fRVDFXc8fq2uZiBhgtr4F8vLVf6jn3EBFH2PAeaz8A6YGnNs2KAGBGZ6pSMj4TNZqAuNLyTSwaumFkjPDOzBwW6c0FRg900Lk53Yq2jl020vclGOloHquhLPsK4V+Dt5SwrJ4FyIPm5RK1pmIjj8Kk1aWPlN5MYFRaWfC1c+faKWGcwJc4hdDIYJvPbL60uRib++vXirs5qf2MTytP6/+OhEs6F1owQ==AQAB
--------------------------------------------------------------------------------