JWSLoader   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
wmc 31
lcom 2
cbo 9
dl 0
loc 218
rs 9.8
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A load() 0 4 1
A getSignatureAlgorithmManager() 0 4 1
A verifyWithKey() 0 6 1
A verifyWithKeySet() 0 10 2
A verifySignature() 0 20 4
B getInputToVerify() 0 17 6
A verifySignatures() 0 17 3
A checkSignatures() 0 6 2
A checkJWKSet() 0 6 2
B checkPayload() 0 9 5
A getAlgorithm() 0 14 3
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\Checker\HeaderCheckerManager;
18
use Jose\Component\Core\AlgorithmManager;
19
use Jose\Component\Core\JWK;
20
use Jose\Component\Core\JWKSet;
21
use Jose\Component\Core\Util\KeyChecker;
22
use Jose\Component\Signature\Algorithm\SignatureAlgorithmInterface;
23
use Jose\Component\Signature\Serializer\JWSSerializerManager;
24
25
/**
26
 * Class able to load JWS and verify signatures and headers.
27
 */
28
final class JWSLoader
29
{
30
    /**
31
     * @var AlgorithmManager
32
     */
33
    private $signatureAlgorithmManager;
34
35
    /**
36
     * @var HeaderCheckerManager
37
     */
38
    private $headerCheckerManager;
39
40
    /**
41
     * @var JWSSerializerManager
42
     */
43
    private $serializerManager;
44
45
    /**
46
     * JWSLoader constructor.
47
     *
48
     * @param AlgorithmManager     $signatureAlgorithmManager
49
     * @param HeaderCheckerManager $headerCheckerManager
50
     * @param JWSSerializerManager $serializerManager
51
     */
52
    public function __construct(AlgorithmManager $signatureAlgorithmManager, HeaderCheckerManager $headerCheckerManager, JWSSerializerManager $serializerManager)
53
    {
54
        $this->signatureAlgorithmManager = $signatureAlgorithmManager;
55
        $this->headerCheckerManager = $headerCheckerManager;
56
        $this->serializerManager = $serializerManager;
57
    }
58
59
    /**
60
     * @param string      $input
61
     * @param string|null $serializer
62
     *
63
     * @return JWS
64
     */
65
    public function load(string $input, ?string &$serializer = null): JWS
66
    {
67
        return $this->serializerManager->unserialize($input, $serializer);
68
    }
69
70
    /**
71
     * @return AlgorithmManager
72
     */
73
    public function getSignatureAlgorithmManager(): AlgorithmManager
74
    {
75
        return $this->signatureAlgorithmManager;
76
    }
77
78
    /**
79
     * @param JWS         $jws
80
     * @param JWK         $jwk
81
     * @param null|string $detachedPayload
82
     *
83
     * @return int If the JWS has been verified, an integer that represents the ID of the signature is set
84
     */
85
    public function verifyWithKey(JWS $jws, JWK $jwk, ?string $detachedPayload = null): int
86
    {
87
        $jwkset = JWKSet::createFromKeys([$jwk]);
88
89
        return $this->verifyWithKeySet($jws, $jwkset, $detachedPayload);
90
    }
91
92
    /**
93
     * Verify the signature of the input.
94
     * The input must be a valid JWS. This method is usually called after the "load" method.
95
     *
96
     * @param JWS         $jws             A JWS object
97
     * @param JWKSet      $jwkset          The signature will be verified using keys in the key set
98
     * @param null|string $detachedPayload If not null, the value must be the detached payload encoded in Base64 URL safe. If the input contains a payload, throws an exception.
99
     *
100
     * @return int If the JWS has been verified, an integer that represents the ID of the signature is set
101
     */
102
    public function verifyWithKeySet(JWS $jws, JWKSet $jwkset, ?string $detachedPayload = null): int
103
    {
104
        $signatureIndex = $this->verifySignatures($jws, $jwkset, $detachedPayload);
105
        if (null === $signatureIndex) {
106
            throw new \InvalidArgumentException('Unable to verify the JWS.');
107
        }
108
        $this->headerCheckerManager->checkJWS($jws, $signatureIndex);
109
110
        return $signatureIndex;
111
    }
112
113
    /**
114
     * @param JWS         $jws
115
     * @param JWKSet      $jwkset
116
     * @param Signature   $signature
117
     * @param null|string $detachedPayload
118
     *
119
     * @return bool
120
     */
121
    private function verifySignature(JWS $jws, JWKSet $jwkset, Signature $signature, ?string $detachedPayload = null): bool
122
    {
123
        $input = $this->getInputToVerify($jws, $signature, $detachedPayload);
124
        foreach ($jwkset->all() as $jwk) {
125
            $algorithm = $this->getAlgorithm($signature);
126
127
            try {
128
                KeyChecker::checkKeyUsage($jwk, 'verification');
129
                KeyChecker::checkKeyAlgorithm($jwk, $algorithm->name());
130
                if (true === $algorithm->verify($jwk, $input, $signature->getSignature())) {
131
                    return true;
132
                }
133
            } catch (\Exception $e) {
134
                //We do nothing, we continue with other keys
135
                continue;
136
            }
137
        }
138
139
        return false;
140
    }
141
142
    /**
143
     * @param JWS         $jws
144
     * @param Signature   $signature
145
     * @param string|null $detachedPayload
146
     *
147
     * @return string
148
     */
149
    private function getInputToVerify(JWS $jws, Signature $signature, ?string $detachedPayload): string
150
    {
151
        $encodedProtectedHeaders = $signature->getEncodedProtectedHeaders();
152
        if (!$signature->hasProtectedHeader('b64') || true === $signature->getProtectedHeader('b64')) {
153
            if (null !== $jws->getEncodedPayload()) {
154
                return sprintf('%s.%s', $encodedProtectedHeaders, $jws->getEncodedPayload());
155
            }
156
157
            $payload = empty($jws->getPayload()) ? $detachedPayload : $jws->getPayload();
158
159
            return sprintf('%s.%s', $encodedProtectedHeaders, Base64Url::encode($payload));
160
        }
161
162
        $payload = empty($jws->getPayload()) ? $detachedPayload : $jws->getPayload();
163
164
        return sprintf('%s.%s', $encodedProtectedHeaders, $payload);
165
    }
166
167
    /**
168
     * @param JWS         $jws
169
     * @param JWKSet      $jwkset
170
     * @param string|null $detachedPayload
171
     *
172
     * @return null|int
173
     */
174
    private function verifySignatures(JWS $jws, JWKSet $jwkset, ?string $detachedPayload = null): ?int
175
    {
176
        $this->checkJWKSet($jwkset);
177
        $this->checkSignatures($jws);
178
        $this->checkPayload($jws, $detachedPayload);
179
180
        $nbSignatures = $jws->countSignatures();
181
182
        for ($i = 0; $i < $nbSignatures; ++$i) {
183
            $signature = $jws->getSignature($i);
184
            if (true === $this->verifySignature($jws, $jwkset, $signature, $detachedPayload)) {
185
                return $i;
186
            }
187
        }
188
189
        return null;
190
    }
191
192
    /**
193
     * @param JWS $jws
194
     */
195
    private function checkSignatures(JWS $jws)
196
    {
197
        if (0 === $jws->countSignatures()) {
198
            throw new \InvalidArgumentException('The JWS does not contain any signature.');
199
        }
200
    }
201
202
    /**
203
     * @param JWKSet $jwkset
204
     */
205
    private function checkJWKSet(JWKSet $jwkset)
206
    {
207
        if (0 === count($jwkset)) {
208
            throw new \InvalidArgumentException('There is no key in the key set.');
209
        }
210
    }
211
212
    /**
213
     * @param JWS         $jws
214
     * @param null|string $detachedPayload
215
     */
216
    private function checkPayload(JWS $jws, ?string $detachedPayload = null)
217
    {
218
        if (null !== $detachedPayload && !empty($jws->getPayload())) {
219
            throw new \InvalidArgumentException('A detached payload is set, but the JWS already has a payload.');
220
        }
221
        if (empty($jws->getPayload()) && null === $detachedPayload) {
222
            throw new \InvalidArgumentException('The JWS has a detached payload, but no payload is provided.');
223
        }
224
    }
225
226
    /**
227
     * @param Signature $signature
228
     *
229
     * @return SignatureAlgorithmInterface
230
     */
231
    private function getAlgorithm(Signature $signature): SignatureAlgorithmInterface
232
    {
233
        $completeHeaders = array_merge($signature->getProtectedHeaders(), $signature->getHeaders());
234
        if (!array_key_exists('alg', $completeHeaders)) {
235
            throw new \InvalidArgumentException('No "alg" parameter set in the header.');
236
        }
237
238
        $algorithm = $this->signatureAlgorithmManager->get($completeHeaders['alg']);
239
        if (!$algorithm instanceof SignatureAlgorithmInterface) {
240
            throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported or is not a signature algorithm.', $completeHeaders['alg']));
241
        }
242
243
        return $algorithm;
244
    }
245
}
246