Failed Conditions
Push — v7 ( 722dd5...1e67af )
by Florent
03:17
created

JWSBuilder::getInputToSign()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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