JWSVerifier::verifySignature()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.584
c 0
b 0
f 0
cc 4
nc 6
nop 5
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2019 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 InvalidArgumentException;
18
use Jose\Component\Core\Algorithm;
19
use Jose\Component\Core\AlgorithmManager;
20
use Jose\Component\Core\JWK;
21
use Jose\Component\Core\JWKSet;
22
use Jose\Component\Core\Util\KeyChecker;
23
use Jose\Component\Signature\Algorithm\MacAlgorithm;
24
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
25
use Throwable;
26
27
class JWSVerifier
28
{
29
    /**
30
     * @var AlgorithmManager
31
     */
32
    private $signatureAlgorithmManager;
33
34
    /**
35
     * JWSVerifier constructor.
36
     */
37
    public function __construct(AlgorithmManager $signatureAlgorithmManager)
38
    {
39
        $this->signatureAlgorithmManager = $signatureAlgorithmManager;
40
    }
41
42
    /**
43
     * Returns the algorithm manager associated to the JWSVerifier.
44
     */
45
    public function getSignatureAlgorithmManager(): AlgorithmManager
46
    {
47
        return $this->signatureAlgorithmManager;
48
    }
49
50
    /**
51
     * This method will try to verify the JWS object using the given key and for the given signature.
52
     * It returns true if the signature is verified, otherwise false.
53
     *
54
     * @return bool true if the verification of the signature succeeded, else false
55
     */
56
    public function verifyWithKey(JWS $jws, JWK $jwk, int $signature, ?string $detachedPayload = null): bool
57
    {
58
        $jwkset = new JWKSet([$jwk]);
59
60
        return $this->verifyWithKeySet($jws, $jwkset, $signature, $detachedPayload);
61
    }
62
63
    /**
64
     * This method will try to verify the JWS object using the given key set and for the given signature.
65
     * It returns true if the signature is verified, otherwise false.
66
     *
67
     * @param JWS         $jws             A JWS object
68
     * @param JWKSet      $jwkset          The signature will be verified using keys in the key set
69
     * @param JWK         $jwk             The key used to verify the signature in case of success
70
     * @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.
71
     *
72
     * @throws InvalidArgumentException if there is no key in the keyset
73
     * @throws InvalidArgumentException if the token does not contain any signature
74
     *
75
     * @return bool true if the verification of the signature succeeded, else false
76
     */
77
    public function verifyWithKeySet(JWS $jws, JWKSet $jwkset, int $signatureIndex, ?string $detachedPayload = null, JWK &$jwk = null): bool
78
    {
79
        if (0 === $jwkset->count()) {
80
            throw new InvalidArgumentException('There is no key in the key set.');
81
        }
82
        if (0 === $jws->countSignatures()) {
83
            throw new InvalidArgumentException('The JWS does not contain any signature.');
84
        }
85
        $this->checkPayload($jws, $detachedPayload);
86
        $signature = $jws->getSignature($signatureIndex);
87
88
        return $this->verifySignature($jws, $jwkset, $signature, $detachedPayload, $jwk);
89
    }
90
91
    private function verifySignature(JWS $jws, JWKSet $jwkset, Signature $signature, ?string $detachedPayload = null, JWK &$successJwk = null): bool
92
    {
93
        $input = $this->getInputToVerify($jws, $signature, $detachedPayload);
94
        $algorithm = $this->getAlgorithm($signature);
95
        foreach ($jwkset->all() as $jwk) {
96
            try {
97
                KeyChecker::checkKeyUsage($jwk, 'verification');
98
                KeyChecker::checkKeyAlgorithm($jwk, $algorithm->name());
99
                if (true === $algorithm->verify($jwk, $input, $signature->getSignature())) {
100
                    $successJwk = $jwk;
101
102
                    return true;
103
                }
104
            } catch (Throwable $e) {
105
                //We do nothing, we continue with other keys
106
                continue;
107
            }
108
        }
109
110
        return false;
111
    }
112
113
    private function getInputToVerify(JWS $jws, Signature $signature, ?string $detachedPayload): string
114
    {
115
        $isPayloadEmpty = $this->isPayloadEmpty($jws->getPayload());
116
        $encodedProtectedHeader = $signature->getEncodedProtectedHeader();
117
        if (!$signature->hasProtectedHeaderParameter('b64') || true === $signature->getProtectedHeaderParameter('b64')) {
118
            if (null !== $jws->getEncodedPayload()) {
119
                return sprintf('%s.%s', $encodedProtectedHeader, $jws->getEncodedPayload());
120
            }
121
122
            $payload = $isPayloadEmpty ? $detachedPayload : $jws->getPayload();
123
124
            return sprintf('%s.%s', $encodedProtectedHeader, Base64Url::encode($payload));
125
        }
126
127
        $payload = $isPayloadEmpty ? $detachedPayload : $jws->getPayload();
128
129
        return sprintf('%s.%s', $encodedProtectedHeader, $payload);
130
    }
131
132
    /**
133
     * @throws InvalidArgumentException if the payload is set when a detached payload is provided or no payload is defined
134
     */
135
    private function checkPayload(JWS $jws, ?string $detachedPayload = null): void
136
    {
137
        $isPayloadEmpty = $this->isPayloadEmpty($jws->getPayload());
138
        if (null !== $detachedPayload && !$isPayloadEmpty) {
139
            throw new InvalidArgumentException('A detached payload is set, but the JWS already has a payload.');
140
        }
141
        if ($isPayloadEmpty && null === $detachedPayload) {
142
            throw new InvalidArgumentException('The JWS has a detached payload, but no payload is provided.');
143
        }
144
    }
145
146
    /**
147
     * @throws InvalidArgumentException if the header parameter "alg" is missing or invalid
148
     *
149
     * @return MacAlgorithm|SignatureAlgorithm
150
     */
151
    private function getAlgorithm(Signature $signature): Algorithm
152
    {
153
        $completeHeader = array_merge($signature->getProtectedHeader(), $signature->getHeader());
154
        if (!isset($completeHeader['alg'])) {
155
            throw new InvalidArgumentException('No "alg" parameter set in the header.');
156
        }
157
158
        $algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
159
        if (!$algorithm instanceof SignatureAlgorithm && !$algorithm instanceof MacAlgorithm) {
160
            throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported or is not a signature or MAC algorithm.', $completeHeader['alg']));
161
        }
162
163
        return $algorithm;
164
    }
165
166
    private function isPayloadEmpty(?string $payload): bool
167
    {
168
        return null === $payload || '' === $payload;
169
    }
170
}
171