Failed Conditions
Push — master ( 775fad...7c0052 )
by Florent
02:07
created

JWSLoader::checkPayload()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 5
nc 3
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\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
        $this->checkJWKSet($jwkset);
105
        $this->checkSignatures($jws);
106
        $this->checkPayload($jws, $detachedPayload);
107
108
        $nbSignatures = $jws->countSignatures();
109
110
        for ($i = 0; $i < $nbSignatures; ++$i) {
111
            try {
112
                $this->headerCheckerManager->check($jws, $i);
113
            } catch (\Exception $e) {
114
                continue;
115
            }
116
            $signature = $jws->getSignature($i);
117
            if (true === $this->verifySignature($jws, $jwkset, $signature, $detachedPayload)) {
118
                return $i;
119
            }
120
        }
121
122
        throw new \InvalidArgumentException('Unable to verify the JWS.');
123
    }
124
125
    /**
126
     * @param JWS         $jws
127
     * @param JWKSet      $jwkset
128
     * @param Signature   $signature
129
     * @param null|string $detachedPayload
130
     *
131
     * @return bool
132
     */
133
    private function verifySignature(JWS $jws, JWKSet $jwkset, Signature $signature, ?string $detachedPayload = null): bool
134
    {
135
        $input = $this->getInputToVerify($jws, $signature, $detachedPayload);
136
        foreach ($jwkset->all() as $jwk) {
137
            $algorithm = $this->getAlgorithm($signature);
138
139
            try {
140
                KeyChecker::checkKeyUsage($jwk, 'verification');
141
                KeyChecker::checkKeyAlgorithm($jwk, $algorithm->name());
142
                if (true === $algorithm->verify($jwk, $input, $signature->getSignature())) {
143
                    return true;
144
                }
145
            } catch (\Exception $e) {
146
                //We do nothing, we continue with other keys
147
                continue;
148
            }
149
        }
150
151
        return false;
152
    }
153
154
    /**
155
     * @param JWS         $jws
156
     * @param Signature   $signature
157
     * @param string|null $detachedPayload
158
     *
159
     * @return string
160
     */
161
    private function getInputToVerify(JWS $jws, Signature $signature, ?string $detachedPayload): string
162
    {
163
        $encodedProtectedHeaders = $signature->getEncodedProtectedHeaders();
164
        if (!$signature->hasProtectedHeader('b64') || true === $signature->getProtectedHeader('b64')) {
165
            if (null !== $jws->getEncodedPayload()) {
166
                return sprintf('%s.%s', $encodedProtectedHeaders, $jws->getEncodedPayload());
167
            }
168
169
            $payload = empty($jws->getPayload()) ? $detachedPayload : $jws->getPayload();
170
171
            return sprintf('%s.%s', $encodedProtectedHeaders, Base64Url::encode($payload));
172
        }
173
174
        $payload = empty($jws->getPayload()) ? $detachedPayload : $jws->getPayload();
175
176
        return sprintf('%s.%s', $encodedProtectedHeaders, $payload);
177
    }
178
179
    /**
180
     * @param JWS $jws
181
     */
182
    private function checkSignatures(JWS $jws)
183
    {
184
        if (0 === $jws->countSignatures()) {
185
            throw new \InvalidArgumentException('The JWS does not contain any signature.');
186
        }
187
    }
188
189
    /**
190
     * @param JWKSet $jwkset
191
     */
192
    private function checkJWKSet(JWKSet $jwkset)
193
    {
194
        if (0 === count($jwkset)) {
195
            throw new \InvalidArgumentException('There is no key in the key set.');
196
        }
197
    }
198
199
    /**
200
     * @param JWS         $jws
201
     * @param null|string $detachedPayload
202
     */
203
    private function checkPayload(JWS $jws, ?string $detachedPayload = null)
204
    {
205
        if (null !== $detachedPayload && !empty($jws->getPayload())) {
206
            throw new \InvalidArgumentException('A detached payload is set, but the JWS already has a payload.');
207
        }
208
        if (empty($jws->getPayload()) && null === $detachedPayload) {
209
            throw new \InvalidArgumentException('The JWS has a detached payload, but no payload is provided.');
210
        }
211
    }
212
213
    /**
214
     * @param Signature $signature
215
     *
216
     * @return SignatureAlgorithmInterface
217
     */
218
    private function getAlgorithm(Signature $signature): SignatureAlgorithmInterface
219
    {
220
        $completeHeaders = array_merge($signature->getProtectedHeaders(), $signature->getHeaders());
221
        if (!array_key_exists('alg', $completeHeaders)) {
222
            throw new \InvalidArgumentException('No "alg" parameter set in the header.');
223
        }
224
225
        $algorithm = $this->signatureAlgorithmManager->get($completeHeaders['alg']);
226
        if (!$algorithm instanceof SignatureAlgorithmInterface) {
227
            throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported or is not a signature algorithm.', $completeHeaders['alg']));
228
        }
229
230
        return $algorithm;
231
    }
232
}
233