Failed Conditions
Push — master ( 6560de...8483ce )
by Florent
05:17
created

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