1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace XmlSigner; |
4
|
|
|
|
5
|
|
|
use XmlSigner\Exception\SignerException; |
6
|
|
|
use XmlSigner\Validator\XmlValidator; |
7
|
|
|
use DOMDocument; |
8
|
|
|
use DOMNode; |
9
|
|
|
|
10
|
|
|
class Signer |
11
|
|
|
{ |
12
|
|
|
const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#'; |
13
|
|
|
const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; |
14
|
|
|
const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; |
15
|
|
|
const SHA1_SIG = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; |
16
|
|
|
const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'; |
17
|
|
|
const SHA256_SIG = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; |
18
|
|
|
|
19
|
|
|
private $algorithm; |
20
|
|
|
private $canonical; |
21
|
|
|
private $certificate; |
22
|
|
|
|
23
|
3 |
|
public function __construct(Certificate $certificate) |
24
|
|
|
{ |
25
|
3 |
|
$this->certificate = $certificate; |
26
|
3 |
|
} |
27
|
|
|
|
28
|
3 |
|
public function xml( |
29
|
|
|
$content, |
30
|
|
|
$tagName, |
31
|
|
|
$rootName = '', |
32
|
|
|
$algorithm = OPENSSL_ALGO_SHA1, |
33
|
|
|
$canonical = [true, false, null, null] |
34
|
|
|
) { |
35
|
3 |
|
$this->assertValidContent($content); |
36
|
2 |
|
$this->algorithm = $algorithm; |
37
|
2 |
|
$this->canonical = $canonical; |
38
|
|
|
|
39
|
2 |
|
$dom = new DOMDocument('1.0', 'UTF-8'); |
40
|
2 |
|
$dom->loadXML($content); |
41
|
2 |
|
$dom->preserveWhiteSpace = false; |
42
|
2 |
|
$dom->formatOutput = false; |
43
|
2 |
|
$root = $dom->documentElement; |
44
|
2 |
|
if (!empty($rootName)) { |
45
|
|
|
$root = $dom->getElementsByTagName($rootName)->item(0); |
46
|
|
|
} |
47
|
2 |
|
$node = $dom->getElementsByTagName($tagName)->item(0); |
48
|
2 |
|
if (empty($node) || empty($root)) { |
49
|
1 |
|
throw SignerException::tagNotFound($tagName); |
50
|
|
|
} |
51
|
1 |
|
$dom = $this->createSignature( |
52
|
1 |
|
$dom, |
53
|
1 |
|
$root, |
54
|
1 |
|
$node |
55
|
|
|
); |
56
|
1 |
|
return (string) '<?xml version="1.0" encoding="UTF-8"?>' |
57
|
1 |
|
. $dom->saveXML($dom->documentElement, LIBXML_NOXMLDECL); |
58
|
|
|
} |
59
|
|
|
|
60
|
3 |
|
private function assertValidContent($content) |
61
|
|
|
{ |
62
|
3 |
|
if (!XmlValidator::valid($content)) { |
63
|
1 |
|
throw SignerException::invalidContent(); |
64
|
|
|
} |
65
|
2 |
|
} |
66
|
|
|
|
67
|
1 |
|
private function createSignature( |
68
|
|
|
DOMDocument $dom, |
69
|
|
|
DOMNode $root, |
70
|
|
|
DOMNode $node |
71
|
|
|
) { |
72
|
1 |
|
$algorithmData = $this->algorithmData(); |
73
|
1 |
|
$nsSignatureMethod = $algorithmData['nsSignatureMethod']; |
74
|
1 |
|
$nsDigestMethod = $algorithmData['nsDigestMethod']; |
75
|
1 |
|
$digestAlgorithm = $algorithmData['digestAlgorithm']; |
76
|
|
|
|
77
|
1 |
|
$digestValue = $this->makeDigest($node, $digestAlgorithm); |
78
|
1 |
|
$signatureNode = $dom->createElementNS(self::XMLDSIGNS, 'Signature'); |
79
|
1 |
|
$root->appendChild($signatureNode); |
80
|
|
|
|
81
|
1 |
|
$signedInfoNode = $dom->createElement('SignedInfo'); |
82
|
1 |
|
$signatureNode->appendChild($signedInfoNode); |
83
|
1 |
|
$canonicalNode = $dom->createElement('CanonicalizationMethod'); |
84
|
1 |
|
$signedInfoNode->appendChild($canonicalNode); |
85
|
1 |
|
$canonicalNode->setAttribute('Algorithm', self::C14N); |
86
|
1 |
|
$signatureMethodNode = $dom->createElement('SignatureMethod'); |
87
|
1 |
|
$signedInfoNode->appendChild($signatureMethodNode); |
88
|
1 |
|
$signatureMethodNode->setAttribute('Algorithm', $nsSignatureMethod); |
89
|
1 |
|
$referenceNode = $dom->createElement('Reference'); |
90
|
1 |
|
$signedInfoNode->appendChild($referenceNode); |
91
|
1 |
|
$transformsNode = $dom->createElement('Transforms'); |
92
|
1 |
|
$referenceNode->appendChild($transformsNode); |
93
|
1 |
|
$transfNode1 = $dom->createElement('Transform'); |
94
|
1 |
|
$transformsNode->appendChild($transfNode1); |
95
|
1 |
|
$transfNode1->setAttribute('Algorithm', self::XMLDSIGNS.'enveloped-signature'); |
96
|
1 |
|
$transfNode2 = $dom->createElement('Transform'); |
97
|
1 |
|
$transformsNode->appendChild($transfNode2); |
98
|
1 |
|
$transfNode2->setAttribute('Algorithm', self::C14N); |
99
|
1 |
|
$digestMethodNode = $dom->createElement('DigestMethod'); |
100
|
1 |
|
$referenceNode->appendChild($digestMethodNode); |
101
|
1 |
|
$digestMethodNode->setAttribute('Algorithm', $nsDigestMethod); |
102
|
1 |
|
$digestValueNode = $dom->createElement('DigestValue', $digestValue); |
103
|
1 |
|
$referenceNode->appendChild($digestValueNode); |
104
|
|
|
|
105
|
1 |
|
$c14n = $this->canonize($signedInfoNode); |
106
|
1 |
|
$signature = $this->certificate->sign($c14n, $this->algorithm); |
107
|
1 |
|
$signatureValue = base64_encode($signature); |
108
|
1 |
|
$signatureValueNode = $dom->createElement('SignatureValue', $signatureValue); |
109
|
1 |
|
$signatureNode->appendChild($signatureValueNode); |
110
|
|
|
|
111
|
1 |
|
$keyInfoNode = $dom->createElement('KeyInfo'); |
112
|
1 |
|
$signatureNode->appendChild($keyInfoNode); |
113
|
1 |
|
$x509DataNode = $dom->createElement('X509Data'); |
114
|
1 |
|
$keyInfoNode->appendChild($x509DataNode); |
115
|
1 |
|
$pubKeyClean = $this->certificate->publicKey(); |
116
|
1 |
|
$x509CertificateNode = $dom->createElement('X509Certificate', $pubKeyClean); |
117
|
1 |
|
$x509DataNode->appendChild($x509CertificateNode); |
118
|
|
|
|
119
|
1 |
|
return $dom; |
120
|
|
|
} |
121
|
|
|
|
122
|
1 |
|
private function algorithmData() |
123
|
|
|
{ |
124
|
1 |
|
switch ($this->algorithm) { |
125
|
1 |
|
case OPENSSL_ALGO_SHA256: |
126
|
|
|
$digestAlgorithm = 'sha256'; |
127
|
|
|
$nsSignatureMethod = self::SHA256_SIG; |
128
|
|
|
$nsDigestMethod = self::SHA256; |
129
|
|
|
break; |
130
|
|
|
default: |
131
|
1 |
|
$nsSignatureMethod = self::SHA1_SIG; |
132
|
1 |
|
$nsDigestMethod = self::SHA1; |
133
|
1 |
|
$digestAlgorithm = 'sha1'; |
134
|
1 |
|
break; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
return [ |
138
|
1 |
|
'digestAlgorithm' => $digestAlgorithm, |
139
|
1 |
|
'nsSignatureMethod' => $nsSignatureMethod, |
140
|
1 |
|
'nsDigestMethod' => $nsDigestMethod |
141
|
|
|
]; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Calculate digest value for given node |
146
|
|
|
* @param DOMNode $node |
147
|
|
|
* @param string $algorithm |
148
|
|
|
* @return string |
149
|
|
|
*/ |
150
|
1 |
|
private function makeDigest(DOMNode $node, $algorithm) |
151
|
|
|
{ |
152
|
1 |
|
$c14n = $this->canonize($node); |
153
|
1 |
|
$hashValue = hash($algorithm, $c14n, true); |
154
|
1 |
|
return base64_encode($hashValue); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Reduced to the canonical form |
159
|
|
|
* @param DOMNode $node |
160
|
|
|
* @return string |
161
|
|
|
*/ |
162
|
1 |
|
private function canonize(DOMNode $node) |
163
|
|
|
{ |
164
|
1 |
|
return $node->C14N( |
165
|
1 |
|
$this->canonical[0], |
166
|
1 |
|
$this->canonical[1], |
167
|
1 |
|
$this->canonical[2], |
168
|
1 |
|
$this->canonical[3] |
169
|
|
|
); |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|