Failed Conditions
Push — master ( 5537f8...d96005 )
by Florent
02:00
created

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