Failed Conditions
Push — v7 ( 73ebba...e5c6da )
by Florent
02:18
created

JWSLoader::verifySignatures()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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