JWSBuilder::findSignatureAlgorithm()   A
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.0777
c 0
b 0
f 0
cc 6
nc 4
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2019 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace Jose\Component\Signature;
15
16
use Base64Url\Base64Url;
17
use InvalidArgumentException;
18
use Jose\Component\Core\Algorithm;
19
use Jose\Component\Core\AlgorithmManager;
20
use Jose\Component\Core\JWK;
21
use Jose\Component\Core\Util\JsonConverter;
22
use Jose\Component\Core\Util\KeyChecker;
23
use Jose\Component\Signature\Algorithm\MacAlgorithm;
24
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
25
use LogicException;
26
use RuntimeException;
27
28
class JWSBuilder
29
{
30
    /**
31
     * @var null|string
32
     */
33
    protected $payload;
34
35
    /**
36
     * @var bool
37
     */
38
    protected $isPayloadDetached;
39
40
    /**
41
     * @var array
42
     */
43
    protected $signatures = [];
44
45
    /**
46
     * @var null|bool
47
     */
48
    protected $isPayloadEncoded;
49
50
    /**
51
     * @var AlgorithmManager
52
     */
53
    private $signatureAlgorithmManager;
54
55
    public function __construct(AlgorithmManager $signatureAlgorithmManager)
56
    {
57
        $this->signatureAlgorithmManager = $signatureAlgorithmManager;
58
    }
59
60
    /**
61
     * Returns the algorithm manager associated to the builder.
62
     */
63
    public function getSignatureAlgorithmManager(): AlgorithmManager
64
    {
65
        return $this->signatureAlgorithmManager;
66
    }
67
68
    /**
69
     * Reset the current data.
70
     *
71
     * @return JWSBuilder
72
     */
73
    public function create(): self
74
    {
75
        $this->payload = null;
76
        $this->isPayloadDetached = false;
77
        $this->signatures = [];
78
        $this->isPayloadEncoded = null;
79
80
        return $this;
81
    }
82
83
    /**
84
     * Set the payload.
85
     * This method will return a new JWSBuilder object.
86
     *
87
     * @throws InvalidArgumentException if the payload is not UTF-8 encoded
88
     *
89
     * @return JWSBuilder
90
     */
91
    public function withPayload(string $payload, bool $isPayloadDetached = false): self
92
    {
93
        if (false === mb_detect_encoding($payload, 'UTF-8', true)) {
94
            throw new InvalidArgumentException('The payload must be encoded in UTF-8');
95
        }
96
        $clone = clone $this;
97
        $clone->payload = $payload;
98
        $clone->isPayloadDetached = $isPayloadDetached;
99
100
        return $clone;
101
    }
102
103
    /**
104
     * Adds the information needed to compute the signature.
105
     * This method will return a new JWSBuilder object.
106
     *
107
     * @throws InvalidArgumentException if the payload encoding is inconsistent
108
     *
109
     * @return JWSBuilder
110
     */
111
    public function addSignature(JWK $signatureKey, array $protectedHeader, array $header = []): self
112
    {
113
        $this->checkB64AndCriticalHeader($protectedHeader);
114
        $isPayloadEncoded = $this->checkIfPayloadIsEncoded($protectedHeader);
115
        if (null === $this->isPayloadEncoded) {
116
            $this->isPayloadEncoded = $isPayloadEncoded;
117
        } elseif ($this->isPayloadEncoded !== $isPayloadEncoded) {
118
            throw new InvalidArgumentException('Foreign payload encoding detected.');
119
        }
120
        $this->checkDuplicatedHeaderParameters($protectedHeader, $header);
121
        KeyChecker::checkKeyUsage($signatureKey, 'signature');
122
        $algorithm = $this->findSignatureAlgorithm($signatureKey, $protectedHeader, $header);
123
        KeyChecker::checkKeyAlgorithm($signatureKey, $algorithm->name());
124
        $clone = clone $this;
125
        $clone->signatures[] = [
126
            'signature_algorithm' => $algorithm,
127
            'signature_key' => $signatureKey,
128
            'protected_header' => $protectedHeader,
129
            'header' => $header,
130
        ];
131
132
        return $clone;
133
    }
134
135
    /**
136
     * Computes all signatures and return the expected JWS object.
137
     *
138
     * @throws RuntimeException if the payload is not set
139
     * @throws RuntimeException if no signature is defined
140
     */
141
    public function build(): JWS
142
    {
143
        if (null === $this->payload) {
144
            throw new RuntimeException('The payload is not set.');
145
        }
146
        if (0 === \count($this->signatures)) {
147
            throw new RuntimeException('At least one signature must be set.');
148
        }
149
150
        $encodedPayload = false === $this->isPayloadEncoded ? $this->payload : Base64Url::encode($this->payload);
151
        $jws = new JWS($this->payload, $encodedPayload, $this->isPayloadDetached);
152
        foreach ($this->signatures as $signature) {
153
            /** @var MacAlgorithm|SignatureAlgorithm $algorithm */
154
            $algorithm = $signature['signature_algorithm'];
155
            /** @var JWK $signatureKey */
156
            $signatureKey = $signature['signature_key'];
157
            /** @var array $protectedHeader */
158
            $protectedHeader = $signature['protected_header'];
159
            /** @var array $header */
160
            $header = $signature['header'];
161
            $encodedProtectedHeader = 0 === \count($protectedHeader) ? null : Base64Url::encode(JsonConverter::encode($protectedHeader));
162
            $input = sprintf('%s.%s', $encodedProtectedHeader, $encodedPayload);
163
            if ($algorithm instanceof SignatureAlgorithm) {
164
                $s = $algorithm->sign($signatureKey, $input);
165
            } else {
166
                $s = $algorithm->hash($signatureKey, $input);
167
            }
168
            $jws = $jws->addSignature($s, $protectedHeader, $encodedProtectedHeader, $header);
169
        }
170
171
        return $jws;
172
    }
173
174
    private function checkIfPayloadIsEncoded(array $protectedHeader): bool
175
    {
176
        return !\array_key_exists('b64', $protectedHeader) || true === $protectedHeader['b64'];
177
    }
178
179
    /**
180
     * @throws LogicException if the header parameter "crit" is missing, invalid or does not contain "b64" when "b64" is set
181
     */
182
    private function checkB64AndCriticalHeader(array $protectedHeader): void
183
    {
184
        if (!\array_key_exists('b64', $protectedHeader)) {
185
            return;
186
        }
187
        if (!\array_key_exists('crit', $protectedHeader)) {
188
            throw new LogicException('The protected header parameter "crit" is mandatory when protected header parameter "b64" is set.');
189
        }
190
        if (!\is_array($protectedHeader['crit'])) {
191
            throw new LogicException('The protected header parameter "crit" must be an array.');
192
        }
193
        if (!\in_array('b64', $protectedHeader['crit'], true)) {
194
            throw new LogicException('The protected header parameter "crit" must contain "b64" when protected header parameter "b64" is set.');
195
        }
196
    }
197
198
    /**
199
     * @throws InvalidArgumentException if the header parameter "alg" is missing or the algorithm is not allowed/not supported
200
     *
201
     * @return MacAlgorithm|SignatureAlgorithm
202
     */
203
    private function findSignatureAlgorithm(JWK $key, array $protectedHeader, array $header): Algorithm
204
    {
205
        $completeHeader = array_merge($header, $protectedHeader);
206
        if (!\array_key_exists('alg', $completeHeader)) {
207
            throw new InvalidArgumentException('No "alg" parameter set in the header.');
208
        }
209
        if ($key->has('alg') && $key->get('alg') !== $completeHeader['alg']) {
210
            throw new InvalidArgumentException(sprintf('The algorithm "%s" is not allowed with this key.', $completeHeader['alg']));
211
        }
212
213
        $algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
214
        if (!$algorithm instanceof SignatureAlgorithm && !$algorithm instanceof MacAlgorithm) {
215
            throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $completeHeader['alg']));
216
        }
217
218
        return $algorithm;
219
    }
220
221
    /**
222
     * @throws InvalidArgumentException if the header contains duplicated entries
223
     */
224
    private function checkDuplicatedHeaderParameters(array $header1, array $header2): void
225
    {
226
        $inter = array_intersect_key($header1, $header2);
227
        if (0 !== \count($inter)) {
228
            throw new InvalidArgumentException(sprintf('The header contains duplicated entries: %s.', implode(', ', array_keys($inter))));
229
        }
230
    }
231
}
232