Failed Conditions
Push — v7 ( d6049e...ebb162 )
by Florent
02:07
created

JWSLoader::getSignatureAlgorithmManager()   A

Complexity

Conditions 1
Paths 1

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