Skip to content

Commit 7b76e53

Browse files
committed
Merge pull request #17 from simplesamlphp/xml-Signature
Rewrite of ds:Signature
2 parents 5e480eb + 22fb324 commit 7b76e53

5 files changed

Lines changed: 299 additions & 139 deletions

File tree

src/Key/X509Certificate.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,10 @@ public function getCertificateDetails(): array
138138
*
139139
* @param string $file The file where the PEM-encoded certificate is stored.
140140
*
141-
* @return \SimpleSAML\XMLSecurity\Key\PublicKey A new X509Certificate key.
141+
* @return \SimpleSAML\XMLSecurity\Key\X509Certificate A new X509Certificate key.
142142
* @throws \SimpleSAML\XMLSecurity\Exception\InvalidArgumentException If the file cannot be read.
143143
*/
144-
public static function fromFile(string $file): PublicKey
144+
public static function fromFile(string $file): X509Certificate
145145
{
146146
return new static(static::readFile($file));
147147
}

src/XML/ds/Signature.php

Lines changed: 124 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -5,249 +5,237 @@
55
namespace SimpleSAML\XMLSecurity\XML\ds;
66

77
use DOMElement;
8-
use Exception;
98
use SimpleSAML\Assert\Assert;
9+
use SimpleSAML\XML\Chunk;
10+
use SimpleSAML\XML\Constants;
1011
use SimpleSAML\XML\Exception\InvalidDOMElementException;
11-
use SimpleSAML\XML\Utils as XMLUtils;
12-
use SimpleSAML\XMLSecurity\Utils\Certificate;
13-
use SimpleSAML\XMLSecurity\Utils\Security;
14-
use SimpleSAML\XMLSecurity\XMLSecurityDSig;
15-
use SimpleSAML\XMLSecurity\XMLSecurityKey;
1612

1713
/**
18-
* Wrapper class for XML signatures
14+
* Class representing a ds:Signature element.
1915
*
2016
* @package simplesamlphp/xml-security
2117
*/
2218
final class Signature extends AbstractDsElement
2319
{
24-
/** @var string */
25-
protected string $algorithm;
26-
27-
/** @var string[] */
28-
protected array $certificates = [];
20+
/** @var \SimpleSAML\XMLSecurity\XML\ds\SignedInfo */
21+
protected SignedInfo $signedInfo;
2922

3023
/** @var \SimpleSAML\XMLSecurity\XML\ds\SignatureValue */
31-
protected SignatureValue $value;
24+
protected SignatureValue $signatureValue;
25+
26+
/** @var \SimpleSAML\XMLSecurity\XML\ds\KeyInfo|null $keyInfo */
27+
protected ?KeyInfo $keyInfo;
3228

33-
/** @var \SimpleSAML\XMLSecurity\XMLSecurityKey|null */
34-
protected ?XMLSecurityKey $key;
29+
/** @var \SimpleSAML\XML\Chunk[] */
30+
protected array $objects;
3531

36-
/** @var \SimpleSAML\XMLSecurity\XMLSecurityDSig */
37-
protected XMLSecurityDSig $signer;
32+
/** @var string|null */
33+
protected ?string $Id;
3834

3935

4036
/**
4137
* Signature constructor.
4238
*
43-
* @param string $algorithm
44-
* @param \SimpleSAML\XMLSecurity\XML\ds\SignatureValue $value
45-
* @param string[] $certificates
46-
* @param \SimpleSAML\XMLSecurity\XMLSecurityKey|null $key
47-
*
48-
* @throws \Exception
39+
* @param \SimpleSAML\XMLSecurity\XML\ds\SignedInfo $signedInfo
40+
* @param \SimpleSAML\XMLSecurity\XML\ds\SignatureValue $signatureValue
41+
* @param \SimpleSAML\XMLSecurity\XML\ds\KeyInfo|null $keyInfo
42+
* @param \SimpleSAML\XML\Chunk[] $objects
43+
* @param string|null $Id
4944
*/
5045
public function __construct(
51-
string $algorithm,
52-
SignatureValue $value,
53-
array $certificates = [],
54-
?XMLSecurityKey $key = null
46+
SignedInfo $signedInfo,
47+
SignatureValue $signatureValue,
48+
?KeyInfo $keyInfo,
49+
array $objects = [],
50+
?string $Id = null
5551
) {
56-
$this->setAlgorithm($algorithm);
57-
$this->setCertificates($certificates);
58-
$this->setKey($key);
59-
60-
$this->signer = new XMLSecurityDSig();
61-
$this->signer->idKeys[] = 'ID';
52+
$this->setSignedInfo($signedInfo);
53+
$this->setSignatureValue($signatureValue);
54+
$this->setKeyInfo($keyInfo);
55+
$this->setObjects($objects);
56+
$this->setId($Id);
6257
}
6358

6459

6560
/**
66-
* Get the algorithm used by this signature.
61+
* Get the Id used for this signature.
6762
*
68-
* @return string
63+
* @return string|null
6964
*/
70-
public function getAlgorithm(): string
65+
public function getId(): ?string
7166
{
72-
return $this->algorithm;
67+
return $this->Id;
7368
}
7469

7570

7671
/**
77-
* Set the algorithm used by this signature.
72+
* Set the Id used for this signature.
7873
*
79-
* @param string $algorithm
74+
* @param string|null $Id
8075
*/
81-
protected function setAlgorithm(string $algorithm): void
76+
protected function setId(?string $Id): void
8277
{
83-
Assert::notWhitespaceOnly($algorithm, 'Signature algorithm cannot be empty');
84-
$this->algorithm = $algorithm;
78+
$this->Id = $Id;
8579
}
8680

8781

8882
/**
89-
* Get the array of certificates attached to this signature.
90-
*
91-
* @return string[]
83+
* @param \SimpleSAML\XMLSecurity\XML\ds\SignedInfo
9284
*/
93-
public function getCertificates(): array
85+
protected function setSignedInfo(SignedInfo $signedInfo): void
9486
{
95-
return $this->certificates;
87+
$this->signedInfo = $signedInfo;
9688
}
9789

9890

9991
/**
100-
* Set the array of certificates (in PEM format) attached to this signature.
101-
*
102-
* @param string[] $certificates
92+
* @return \SimpleSAML\XMLSecurity\XML\ds\SignedInfo
10393
*/
104-
protected function setCertificates(array $certificates): void
94+
public function getSignedInfo(): SignedInfo
10595
{
106-
Assert::allStringNotEmpty($certificates, 'Cannot add empty certificates.');
107-
Assert::allTrue(
108-
array_map([Certificate::class, 'hasValidStructure'], $certificates),
109-
'One or more certificates have an invalid format.'
110-
);
111-
$this->certificates = $certificates;
96+
return $this->signedInfo;
11297
}
11398

11499

115100
/**
116-
* @param \SimpleSAML\XMLSecurity\XMLSecurityKey|null $key
101+
* @param \SimpleSAML\XMLSecurity\XML\ds\SignatureValue
117102
*/
118-
protected function setKey(?XMLSecurityKey $key): void
103+
protected function setSignatureValue(SignatureValue $signatureValue): void
119104
{
120-
if ($key !== null) {
121-
Assert::eq($this->algorithm, $key->getAlgorithm(), 'Key type does not match signature algorithm.');
122-
}
123-
$this->key = $key;
105+
$this->signatureValue = $signatureValue;
124106
}
125107

126108

127109
/**
128-
* Get the SignatureValue corresponding to this signature.
129-
*
130110
* @return \SimpleSAML\XMLSecurity\XML\ds\SignatureValue
131111
*/
132112
public function getSignatureValue(): SignatureValue
133113
{
134-
return $this->value;
114+
return $this->signatureValue;
115+
}
116+
117+
118+
/**
119+
* @param \SimpleSAML\XMLSecurity\XML\ds\KeyInfo|null
120+
*/
121+
protected function setKeyInfo(?KeyInfo $keyInfo): void
122+
{
123+
$this->keyInfo = $keyInfo;
135124
}
136125

137126

138127
/**
139-
* Set the SignatureValue.
128+
* @return \SimpleSAML\XMLSecurity\XML\ds\KeyInfo|null
129+
*/
130+
public function getKeyInfo(): ?KeyInfo
131+
{
132+
return $this->keyInfo;
133+
}
134+
135+
136+
/**
137+
* Get the array of ds:Object elements attached to this signature.
140138
*
141-
* @param \SimpleSAML\XMLSecurity\XML\ds\SignatureValue $value
139+
* @return \SimpleSAML\XML\Chunk[]
142140
*/
143-
protected function setSignatureValue(SignatureValue $value): void
141+
public function getObjects(): array
144142
{
145-
$this->value = $value;
143+
return $this->objects;
146144
}
147145

148146

149147
/**
150-
* @return XMLSecurityDSig
148+
* Set the array of ds:Object elements attached to this signature.
149+
*
150+
* @param \SimpleSAML\XML\Chunk[] $objects
151151
*/
152-
public function getSigner(): XMLSecurityDSig
152+
protected function setObjects(array $objects): void
153153
{
154-
return $this->signer;
154+
Assert::allIsInstanceOf($objects, Chunk::class);
155+
156+
foreach ($objects as $o) {
157+
Assert::true(
158+
$o->getNamespaceURI() === Constants::NS_XDSIG
159+
&& $o->getLocalName() === 'Object',
160+
'Only elements of type ds:Object are allowed.'
161+
);
162+
}
163+
164+
$this->objects = $objects;
155165
}
156166

157167

158168
/**
159-
* @param \DOMElement $xml
169+
* Convert XML into a Signature element
160170
*
171+
* @param \DOMElement $xml
161172
* @return \SimpleSAML\XML\AbstractXMLElement
162-
* @throws \Exception
163173
*
164174
* @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
165175
* If the qualified name of the supplied element is wrong
166-
* @throws \SimpleSAML\XML\Exception\MissingAttributeException
167-
* If the supplied signature is missing an Algorithm attribute
168176
*/
169177
public static function fromXML(DOMElement $xml): object
170178
{
171179
Assert::same($xml->localName, 'Signature', InvalidDOMElementException::class);
172180
Assert::same($xml->namespaceURI, Signature::NS, InvalidDOMElementException::class);
173181

174-
$parent = $xml->parentNode;
175-
176-
$sigMethod = XMLUtils::xpQuery($xml, './ds:SignedInfo/ds:SignatureMethod');
177-
Assert::notEmpty($sigMethod, 'Missing ds:SignatureMethod element.');
178-
/** @var \DOMElement $sigMethod */
179-
$sigMethod = $sigMethod[0];
180-
Assert::true(
181-
$sigMethod->hasAttribute('Algorithm'),
182-
'Missing "Algorithm" attribute on ds:SignatureMethod element.'
183-
);
184-
185-
// now we extract all available X509 certificates in the signature element
186-
$certificates = [];
187-
foreach (XMLUtils::xpQuery($xml, './ds:KeyInfo/ds:X509Data/ds:X509Certificate') as $certNode) {
188-
$certificates[] = Certificate::convertToCertificate(
189-
str_replace(["\r", "\n", "\t", ' '], '', trim($certNode->textContent))
190-
);
191-
}
192-
193-
$value = SignatureValue::getChildrenOfClass($xml);
194-
Assert::count($value, 1, 'ds:Signature needs exactly one ds:SignatureValue');
195-
196-
$signature = new self(self::getAttribute($sigMethod, 'Algorithm'), $value[0], $certificates);
182+
$Id = self::getAttribute($xml, 'Id', null);
197183

198-
$signature->signer->sigNode = $xml;
184+
$signedInfo = SignedInfo::getChildrenOfClass($xml);
185+
Assert::count($signedInfo, 1, 'ds:Signature needs exactly one ds:SignedInfo element.');
199186

200-
// canonicalize the XMLDSig SignedInfo element in the message
201-
$signature->signer->canonicalizeSignedInfo();
187+
$signatureValue = SignatureValue::getChildrenOfClass($xml);
188+
Assert::count($signatureValue, 1, 'ds:Signature needs exactly one ds:SignatureValue element.');
202189

203-
// validate referenced xml nodes
204-
if (!$signature->signer->validateReference()) {
205-
throw new Exception('Digest validation failed.');
206-
}
190+
$keyInfo = KeyInfo::getChildrenOfClass($xml);
191+
Assert::maxCount($keyInfo, 1, 'ds:Signature can hold a maximum of one ds:KeyInfo element.');
207192

208-
// check that $root is one of the signed nodes
209-
$rootSigned = false;
210-
/** @var \DOMNode $signedNode */
211-
foreach ($signature->signer->getValidatedNodes() as $signedNode) {
212-
if ($signedNode->isSameNode($parent)) {
213-
$rootSigned = true;
214-
break;
215-
} elseif ($parent->parentNode instanceof \DOMDocument && $signedNode->isSameNode($parent->ownerDocument)) {
216-
// $parent is the root element of a signed document
217-
$rootSigned = true;
218-
break;
193+
$objects = [];
194+
foreach ($xml->childNodes as $o) {
195+
if (
196+
$o instanceof DOMElement
197+
&& $o->namespaceURI === Constants::NS_XDSIG
198+
&& $o->localName === 'Object'
199+
) {
200+
$objects[] = Chunk::fromXML($o);
219201
}
220202
}
221-
if (!$rootSigned) {
222-
throw new Exception('The parent element is not signed.');
223-
}
224203

225-
return $signature;
204+
return new self(
205+
array_pop($signedInfo),
206+
array_pop($signatureValue),
207+
empty($keyInfo) ? null : array_pop($keyInfo),
208+
$objects,
209+
$Id
210+
);
226211
}
227212

228213

229214
/**
230-
* @param \DOMElement|null $parent
215+
* Convert this Signature element to XML.
231216
*
217+
* @param \DOMElement|null $parent The element we should append this Signature element to.
232218
* @return \DOMElement
233-
*
234-
* @psalm-suppress MoreSpecificReturnType
235219
*/
236220
public function toXML(DOMElement $parent = null): DOMElement
237221
{
238-
Assert::notNull($parent, 'Cannot create a Signature without anything to sign.');
239-
Assert::notNull($this->key, 'Cannot sign without a signing key.');
240-
241-
// find first child element
242-
$childElements = XMLUtils::xpQuery($parent, './*');
243-
$firstChildElement = null;
244-
if (count($childElements) > 0) {
245-
$firstChildElement = $childElements[0];
222+
$e = $this->instantiateParentElement($parent);
223+
224+
if ($this->Id !== null) {
225+
$e->setAttribute('Id', $this->Id);
246226
}
247227

248-
Security::insertSignature($this->key, $this->certificates, $parent, $firstChildElement);
228+
$this->signedInfo->toXML($e);
229+
$this->signatureValue->toXML($e);
230+
231+
if ($this->keyInfo !== null) {
232+
$this->keyInfo->toXML($e);
233+
}
234+
235+
foreach ($this->objects as $o) {
236+
$o->toXML($e);
237+
}
249238

250-
/** @psalm-suppress LessSpecificReturnStatement */
251-
return XMLUtils::xpQuery($parent, './ds:Signature')[0];
239+
return $e;
252240
}
253241
}

tests/SignatureTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use SimpleSAML\XMLSecurity\Exception\InvalidArgumentException;
1010
use SimpleSAML\XMLSecurity\Key\PrivateKey;
1111
use SimpleSAML\XMLSecurity\Key\X509Certificate;
12-
use SimpleSAML\XMLSecurity\Signature;
1312

1413
/**
1514
* Test for XML digital signatures.

0 commit comments

Comments
 (0)