SignableElementTrait::getReference()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 43
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 28
c 0
b 0
f 0
nc 4
nop 4
dl 0
loc 43
rs 9.1608
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XMLSecurity\XML;
6
7
use DOMElement;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\XML\DOMDocumentFactory;
10
use SimpleSAML\XMLSchema\Type\{AnyURIValue, Base64BinaryValue, IDValue};
11
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface;
12
use SimpleSAML\XMLSecurity\Constants as C;
13
use SimpleSAML\XMLSecurity\Exception\{RuntimeException, UnsupportedAlgorithmException};
14
use SimpleSAML\XMLSecurity\Type\DigestValue as DigestValueType;
15
use SimpleSAML\XMLSecurity\Utils\XML;
16
use SimpleSAML\XMLSecurity\XML\ds\{
17
    CanonicalizationMethod,
18
    DigestMethod,
19
    DigestValue,
20
    KeyInfo,
21
    Reference,
22
    Signature,
23
    SignatureMethod,
24
    SignatureValue,
25
    SignedInfo,
26
    Transform,
27
    Transforms,
28
};
29
30
use function base64_encode;
31
use function hash;
32
use function in_array;
33
34
/**
35
 * Trait SignableElementTrait
36
 *
37
 * @package simplesamlphp/xml-security
38
 */
39
trait SignableElementTrait
40
{
41
    use CanonicalizableElementTrait;
42
43
    /** @var \SimpleSAML\XMLSecurity\XML\ds\Signature|null */
44
    protected ?Signature $signature = null;
45
46
    /** @var string */
47
    private string $c14nAlg = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS;
48
49
    /** @var \SimpleSAML\XMLSecurity\XML\ds\KeyInfo|null */
50
    private ?KeyInfo $keyInfo = null;
51
52
    /** @var \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface|null */
53
    protected ?SignatureAlgorithmInterface $signer = null;
54
55
56
    /**
57
     * Get the ID of this element.
58
     *
59
     * When this method returns null, the signature created for this object will reference the entire document.
60
     *
61
     * @return \SimpleSAML\XML\Type\IDValue|null The ID of this element, or null if we don't have one.
62
     */
63
    abstract public function getId(): ?IDValue;
64
65
66
    /**
67
     * Sign the current element.
68
     *
69
     * The signature will not be applied until toXML() is called.
70
     *
71
     * @param \SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmInterface $signer The actual signer implementation
72
     * to use.
73
     * @param string $canonicalizationAlg The identifier of the canonicalization algorithm to use.
74
     * @param \SimpleSAML\XMLSecurity\XML\ds\KeyInfo|null $keyInfo A KeyInfo object to add to the signature.
75
     */
76
    public function sign(
77
        SignatureAlgorithmInterface $signer,
78
        string $canonicalizationAlg = C::C14N_EXCLUSIVE_WITHOUT_COMMENTS,
79
        ?KeyInfo $keyInfo = null,
80
    ): void {
81
        $this->signer = $signer;
82
        $this->keyInfo = $keyInfo;
83
        Assert::oneOf(
84
            $canonicalizationAlg,
85
            [
86
                C::C14N_INCLUSIVE_WITH_COMMENTS,
87
                C::C14N_INCLUSIVE_WITHOUT_COMMENTS,
88
                C::C14N_EXCLUSIVE_WITH_COMMENTS,
89
                C::C14N_EXCLUSIVE_WITHOUT_COMMENTS,
90
            ],
91
            'Unsupported canonicalization algorithm: %s',
92
            UnsupportedAlgorithmException::class,
93
        );
94
        $this->c14nAlg = $canonicalizationAlg;
95
    }
96
97
98
    /**
99
     * Get a ds:Reference pointing to this object.
100
     *
101
     * @param string $digestAlg The digest algorithm to use.
102
     * @param \SimpleSAML\XMLSecurity\XML\ds\Transforms $transforms The transforms to apply to the object.
103
     */
104
    private function getReference(
105
        string $digestAlg,
106
        Transforms $transforms,
107
        DOMElement $xml,
108
        string $canonicalDocument,
109
    ): Reference {
110
        $id = $this->getId();
111
        $uri = null;
112
        if (empty($id)) { // document reference
113
            Assert::notNull(
114
                $xml->ownerDocument->documentElement,
115
                'Cannot create a document reference without a root element in the document.',
116
                RuntimeException::class,
117
            );
118
            Assert::true(
119
                $xml->isSameNode($xml->ownerDocument->documentElement),
120
                'Cannot create a document reference when signing an object that is not the root of the document. ' .
121
                'Please give your object an identifier.',
122
                RuntimeException::class,
123
            );
124
            if (in_array($this->c14nAlg, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
125
                $uri = '#xpointer(/)';
126
            }
127
        } elseif (in_array($this->c14nAlg, [C::C14N_INCLUSIVE_WITH_COMMENTS, C::C14N_EXCLUSIVE_WITH_COMMENTS])) {
128
            // regular reference, but must retain comments
129
            $uri = '#xpointer(id(' . $id . '))';
130
        } else { // regular reference, can ignore comments
131
            $uri = '#' . $id;
132
        }
133
134
        return new Reference(
135
            new DigestMethod(
136
                AnyURIValue::fromString($digestAlg),
137
            ),
138
            new DigestValue(
139
                DigestValueType::fromString(
140
                    base64_encode(hash(C::$DIGEST_ALGORITHMS[$digestAlg], $canonicalDocument, true)),
141
                ),
142
            ),
143
            $transforms,
144
            null,
145
            null,
146
            ($uri !== null) ? AnyURIValue::fromString($uri) : null,
147
        );
148
    }
149
150
151
    /**
152
     * Do the actual signing of the document.
153
     *
154
     * Note that this method does not insert the signature in the returned \DOMElement. The signature will be available
155
     * in $this->signature as a \SimpleSAML\XMLSecurity\XML\ds\Signature object, which can then be converted to XML
156
     * calling toXML() on it, passing the \DOMElement value returned here as a parameter. The resulting \DOMElement
157
     * can then be inserted in the position desired.
158
     *
159
     * E.g.:
160
     *     $xml = // our XML to sign
161
     *     $signedXML = $this->doSign($xml);
162
     *     $signedXML->appendChild($this->signature->toXML($signedXML));
163
     *
164
     * @param \DOMElement $xml The element to sign.
165
     * @return \DOMElement The signed element, without the signature attached to it just yet.
166
     */
167
    protected function doSign(DOMElement $xml): DOMElement
168
    {
169
        Assert::notNull(
170
            $this->signer,
171
            'Cannot call toSignedXML() without calling sign() first.',
172
            RuntimeException::class,
173
        );
174
175
        $algorithm = $this->signer->getAlgorithmId();
0 ignored issues
show
Bug introduced by
The method getAlgorithmId() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
        /** @scrutinizer ignore-call */ 
176
        $algorithm = $this->signer->getAlgorithmId();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
176
        $digest = $this->signer->getDigest();
177
178
        $transforms = new Transforms([
179
            new Transform(
180
                AnyURIValue::fromString(C::XMLDSIG_ENVELOPED),
181
            ),
182
            new Transform(
183
                AnyURIValue::fromString($this->c14nAlg),
184
            ),
185
        ]);
186
187
        $canonicalDocument = XML::processTransforms($transforms, $xml);
188
189
        $signedInfo = new SignedInfo(
190
            new CanonicalizationMethod(
191
                AnyURIValue::fromString($this->c14nAlg),
192
            ),
193
            new SignatureMethod(
194
                AnyURIValue::fromString($algorithm),
195
            ),
196
            [$this->getReference($digest, $transforms, $xml, $canonicalDocument)],
197
        );
198
199
        $signingData = $signedInfo->canonicalize($this->c14nAlg);
200
        $signedData = base64_encode($this->signer->sign($signingData));
0 ignored issues
show
Bug introduced by
It seems like $this->signer->sign($signingData) can also be of type false; however, parameter $string of base64_encode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

200
        $signedData = base64_encode(/** @scrutinizer ignore-type */ $this->signer->sign($signingData));
Loading history...
201
202
        $this->setSignature(
0 ignored issues
show
Bug introduced by
It seems like setSignature() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

202
        $this->/** @scrutinizer ignore-call */ 
203
               setSignature(
Loading history...
203
            new Signature(
204
                $signedInfo,
205
                new SignatureValue(
206
                    Base64BinaryValue::fromString($signedData),
207
                ),
208
                $this->keyInfo,
209
            ),
210
        );
211
        return DOMDocumentFactory::fromString($canonicalDocument)->documentElement;
212
    }
213
214
    /**
215
     * Get the list of algorithms that are blacklisted for any signing operation.
216
     *
217
     * @return string[]|null An array with all algorithm identifiers that are blacklisted, or null to use this
218
     * libraries default.
219
     */
220
    abstract public function getBlacklistedAlgorithms(): ?array;
221
}
222