├── .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 --------------------------------------------------------------------------------