Failed Conditions
Push — v7 ( 8da077...12d27f )
by Florent
02:14
created

JWSBuilder::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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