Failed Conditions
Push — v7 ( d6049e...ebb162 )
by Florent
02:07
created

JWSBuilder::getSignatureAlgorithmManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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