Issues (88)

src/XML/SignableElementTrait.php (3 issues)

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

166
        /** @scrutinizer ignore-call */ 
167
        $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...
167
        $digest = $this->signer->getDigest();
168
169
        $transforms = new Transforms([
170
            new Transform(C::XMLDSIG_ENVELOPED),
171
            new Transform($this->c14nAlg),
172
        ]);
173
174
        $canonicalDocument = XML::processTransforms($transforms, $xml);
175
176
        $signedInfo = new SignedInfo(
177
            new CanonicalizationMethod($this->c14nAlg),
178
            new SignatureMethod($algorithm),
179
            [$this->getReference($digest, $transforms, $xml, $canonicalDocument)],
180
        );
181
182
        $signingData = $signedInfo->canonicalize($this->c14nAlg);
183
        $signedData = base64_encode($this->signer->sign($signingData));
0 ignored issues
show
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

183
        $signedData = base64_encode(/** @scrutinizer ignore-type */ $this->signer->sign($signingData));
Loading history...
184
185
        $this->setSignature(new Signature($signedInfo, new SignatureValue($signedData), $this->keyInfo));
0 ignored issues
show
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

185
        $this->/** @scrutinizer ignore-call */ 
186
               setSignature(new Signature($signedInfo, new SignatureValue($signedData), $this->keyInfo));
Loading history...
186
        return DOMDocumentFactory::fromString($canonicalDocument)->documentElement;
187
    }
188
189
    /**
190
     * Get the list of algorithms that are blacklisted for any signing operation.
191
     *
192
     * @return string[]|null An array with all algorithm identifiers that are blacklisted, or null to use this
193
     * libraries default.
194
     */
195
    abstract public function getBlacklistedAlgorithms(): ?array;
196
}
197