Failed Conditions
Push — v7 ( 6b9564...530848 )
by Florent
04:53
created

JWSBuilder::checkIfPayloadIsEncoded()   A

Complexity

Conditions 2
Paths 2

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 2
eloc 2
nc 2
nop 1
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
     * @param mixed $payload
79
     * @param bool  $isPayloadDetached
80
     *
81
     * @return JWSBuilder
82
     */
83
    public function withPayload($payload, bool $isPayloadDetached = false): JWSBuilder
84
    {
85
        $payload = is_string($payload) ? $payload : $this->jsonConverter->encode($payload);
86
        if (false === mb_detect_encoding($payload, 'UTF-8', true)) {
87
            throw new \InvalidArgumentException('The payload must be encoded in UTF-8');
88
        }
89
        $clone = clone $this;
90
        $clone->payload = $payload;
91
        $clone->isPayloadDetached = $isPayloadDetached;
92
93
        return $clone;
94
    }
95
96
    /**
97
     * @param JWK   $signatureKey
98
     * @param array $protectedHeaders
99
     * @param array $headers
100
     *
101
     * @return JWSBuilder
102
     */
103
    public function addSignature(JWK $signatureKey, array $protectedHeaders, array $headers = []): JWSBuilder
104
    {
105
        $this->checkB64AndCriticalHeader($protectedHeaders);
106
        $isPayloadEncoded = $this->checkIfPayloadIsEncoded($protectedHeaders);
107
        if (null === $this->isPayloadEncoded) {
108
            $this->isPayloadEncoded = $isPayloadEncoded;
109
        } elseif ($this->isPayloadEncoded !== $isPayloadEncoded) {
110
            throw new \InvalidArgumentException('Foreign payload encoding detected.');
111
        }
112
        $this->checkDuplicatedHeaderParameters($protectedHeaders, $headers);
113
        KeyChecker::checkKeyUsage($signatureKey, 'signature');
114
        $signatureAlgorithm = $this->findSignatureAlgorithm($signatureKey, $protectedHeaders, $headers);
115
        KeyChecker::checkKeyAlgorithm($signatureKey, $signatureAlgorithm->name());
116
        $clone = clone $this;
117
        $clone->signatures[] = [
118
            'signature_algorithm' => $signatureAlgorithm,
119
            'signature_key' => $signatureKey,
120
            'protected_headers' => $protectedHeaders,
121
            'headers' => $headers,
122
        ];
123
124
        return $clone;
125
    }
126
127
    /**
128
     * @return JWS
129
     */
130
    public function build(): JWS
131
    {
132
        if (null === $this->payload) {
133
            throw new \RuntimeException('The payload is not set.');
134
        }
135
        if (0 === count($this->signatures)) {
136
            throw new \RuntimeException('At least one signature must be set.');
137
        }
138
139
        $encodedPayload = false === $this->isPayloadEncoded ? $this->payload : Base64Url::encode($this->payload);
140
        $jws = JWS::create($this->payload, $encodedPayload, $this->isPayloadDetached);
141
        foreach ($this->signatures as $signature) {
142
            /** @var SignatureAlgorithmInterface $signatureAlgorithm */
143
            $signatureAlgorithm = $signature['signature_algorithm'];
144
            /** @var JWK $signatureKey */
145
            $signatureKey = $signature['signature_key'];
146
            /** @var array $protectedHeaders */
147
            $protectedHeaders = $signature['protected_headers'];
148
            /** @var array $headers */
149
            $headers = $signature['headers'];
150
            $encodedProtectedHeaders = empty($protectedHeaders) ? null : Base64Url::encode($this->jsonConverter->encode($protectedHeaders));
151
            $input = sprintf('%s.%s', $encodedProtectedHeaders, $encodedPayload);
152
            $s = $signatureAlgorithm->sign($signatureKey, $input);
153
            $jws = $jws->addSignature($s, $protectedHeaders, $encodedProtectedHeaders, $headers);
154
        }
155
156
        return $jws;
157
    }
158
159
    /**
160
     * @param array $protectedHeaders
161
     *
162
     * @return bool
163
     */
164
    private function checkIfPayloadIsEncoded(array $protectedHeaders): bool
165
    {
166
        return !array_key_exists('b64', $protectedHeaders) || true === $protectedHeaders['b64'];
167
    }
168
169
    /**
170
     * @param array $protectedHeaders
171
     */
172
    private function checkB64AndCriticalHeader(array $protectedHeaders)
173
    {
174
        if (!array_key_exists('b64', $protectedHeaders)) {
175
            return;
176
        }
177
        if (!array_key_exists('crit', $protectedHeaders)) {
178
            throw new \LogicException('The protected header parameter "crit" is mandatory when protected header parameter "b64" is set.');
179
        }
180
        if (!is_array($protectedHeaders['crit'])) {
181
            throw new \LogicException('The protected header parameter "crit" must be an array.');
182
        }
183
        if (!in_array('b64', $protectedHeaders['crit'])) {
184
            throw new \LogicException('The protected header parameter "crit" must contain "b64" when protected header parameter "b64" is set.');
185
        }
186
    }
187
188
    /**
189
     * @param array $protectedHeader
190
     * @param array $headers
191
     * @param JWK   $key
192
     *
193
     * @return SignatureAlgorithmInterface
194
     */
195
    private function findSignatureAlgorithm(JWK $key, array $protectedHeader, array $headers): SignatureAlgorithmInterface
196
    {
197
        $completeHeader = array_merge($headers, $protectedHeader);
198
        if (!array_key_exists('alg', $completeHeader)) {
199
            throw new \InvalidArgumentException('No "alg" parameter set in the header.');
200
        }
201
        if ($key->has('alg') && $key->get('alg') !== $completeHeader['alg']) {
202
            throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not allowed with this key.', $completeHeader['alg']));
203
        }
204
205
        $signatureAlgorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
206
        if (!$signatureAlgorithm instanceof SignatureAlgorithmInterface) {
207
            throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $completeHeader['alg']));
208
        }
209
210
        return $signatureAlgorithm;
211
    }
212
213
    /**
214
     * @param array $header1
215
     * @param array $header2
216
     */
217
    private function checkDuplicatedHeaderParameters(array $header1, array $header2)
218
    {
219
        $inter = array_intersect_key($header1, $header2);
220
        if (!empty($inter)) {
221
            throw new \InvalidArgumentException(sprintf('The header contains duplicated entries: %s.', implode(', ', array_keys($inter))));
222
        }
223
    }
224
}
225