Failed Conditions
Push — v7 ( a94305...5a1c51 )
by Florent
02:15
created

JWSBuilder::checkB64AndCriticalHeader()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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