Failed Conditions
Push — v7 ( 9bed55...5d1eb6 )
by Florent
02:58
created

JWSBuilder::addSignature()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 3
dl 0
loc 15
rs 9.4285
c 0
b 0
f 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\Core\JWAManager;
18
use Jose\Component\Core\JWK;
19
use Jose\Component\Core\KeyChecker;
20
21
final class JWSBuilder
22
{
23
    /**
24
     * @var mixed
25
     */
26
    private $payload;
27
28
    /**
29
     * @var bool
30
     */
31
    private $isPayloadDetached;
32
33
    /**
34
     * @var array
35
     */
36
    private $signatures = [];
37
38
    /**
39
     * @var JWAManager
40
     */
41
    private $signatureAlgorithmManager;
42
43
    /**
44
     * Signer constructor.
45
     *
46
     * @param JWAManager $signatureAlgorithmManager
47
     */
48
    public function __construct(JWAManager $signatureAlgorithmManager)
49
    {
50
        $this->signatureAlgorithmManager = $signatureAlgorithmManager;
51
    }
52
53
    /**
54
     * @return string[]
55
     */
56
    public function getSupportedSignatureAlgorithms(): array
57
    {
58
        return $this->signatureAlgorithmManager->list();
59
    }
60
61
    /**
62
     * @param mixed $payload
63
     * @param bool  $isPayloadDetached
64
     *
65
     * @return JWSBuilder
66
     */
67
    public function withPayload($payload, bool $isPayloadDetached = false): JWSBuilder
68
    {
69
        $clone = clone $this;
70
        $clone->payload = $payload;
71
        $clone->isPayloadDetached = $isPayloadDetached;
72
73
        return $clone;
74
    }
75
76
    /**
77
     * @param JWK   $signatureKey
78
     * @param array $protectedHeaders
79
     * @param array $headers
80
     *
81
     * @return JWSBuilder
82
     */
83
    public function addSignature(JWK $signatureKey, array $protectedHeaders, array $headers = []): JWSBuilder
84
    {
85
        KeyChecker::checkKeyUsage($signatureKey, 'signature');
86
        $algorithm = $this->findSignatureAlgorithm($signatureKey, $protectedHeaders, $headers);
87
        KeyChecker::checkKeyAlgorithm($signatureKey, $algorithm->name());
88
        $clone = clone $this;
89
        $clone->signatures[] = [
90
            'signature_algorithm' => $algorithm,
91
            'signature_key' => $signatureKey,
92
            'protected_headers' => $protectedHeaders,
93
            'headers' => $headers,
94
        ];
95
96
        return $clone;
97
    }
98
99
    /**
100
     * @return JWS
101
     */
102
    public function build(): JWS
103
    {
104
        $jws = JWS::create($this->payload, $this->isPayloadDetached);
105
        foreach ($this->signatures as $signature) {
106
            /** @var SignatureAlgorithmInterface $signatureAlgorithm */
107
            $signatureAlgorithm = $signature['signature_algorithm'];
108
            /** @var JWK $signatureKey */
109
            $signatureKey = $signature['signature_key'];
110
            /** @var array $protectedHeaders */
111
            $protectedHeaders = $signature['protected_headers'];
112
            /** @var array $headers */
113
            $headers = $signature['headers'];
114
            $encodedProtectedHeaders = empty($protectedHeaders) ? null : Base64Url::encode(json_encode($protectedHeaders));
115
            $input = $this->getInputToSign($protectedHeaders, $encodedProtectedHeaders);
116
117
            $s = $signatureAlgorithm->sign($signatureKey, $input);
118
            $jws = $jws->addSignature($s, $encodedProtectedHeaders, $headers);
119
        }
120
121
        return $jws;
122
    }
123
124
    /**
125
     * @param array  $protectedHeaders
126
     * @param string $encodedProtectedHeaders
127
     *
128
     * @return string
129
     */
130
    private function getInputToSign(array $protectedHeaders, ?string $encodedProtectedHeaders): string
131
    {
132
        $this->checkB64AndCriticalHeader($protectedHeaders);
133
        if (!array_key_exists('b64', $protectedHeaders) || (array_key_exists('b64', $protectedHeaders) && true === $protectedHeaders['b64'])) {
134
            $encodedPayload = Base64Url::encode(is_string($this->payload) ? $this->payload : json_encode($this->payload));
135
136
            return sprintf('%s.%s', $encodedProtectedHeaders, $encodedPayload);
137
        }
138
139
        return sprintf('%s.%s', $encodedProtectedHeaders, $this->payload);
140
    }
141
142
    /**
143
     * @param array $protectedHeaders
144
     */
145
    private function checkB64AndCriticalHeader(array $protectedHeaders)
146
    {
147
        if (!array_key_exists('b64', $protectedHeaders)) {
148
            return;
149
        }
150
        if (!array_key_exists('crit', $protectedHeaders)) {
151
            throw new \LogicException('The protected header parameter "crit" is mandatory when protected header parameter "b64" is set.');
152
        }
153
        if (!is_array($protectedHeaders['crit'])) {
154
            throw new \LogicException('The protected header parameter "crit" must be an array.');
155
        }
156
        if (!in_array('b64', $protectedHeaders['crit'])) {
157
            throw new \LogicException('The protected header parameter "crit" must contain "b64" when protected header parameter "b64" is set.');
158
        }
159
    }
160
161
    /**
162
     * @param array $protectedHeader
163
     * @param array $headers
164
     * @param JWK   $key
165
     *
166
     * @return SignatureAlgorithmInterface
167
     */
168
    private function findSignatureAlgorithm(JWK $key, array $protectedHeader, array $headers): SignatureAlgorithmInterface
169
    {
170
        $completeHeader = array_merge($headers, $protectedHeader);
171
        if (!array_key_exists('alg', $completeHeader)) {
172
            throw new \InvalidArgumentException('No "alg" parameter set in the header.');
173
        }
174
        if ($key->has('alg') && $key->get('alg') !== $completeHeader['alg']) {
175
            throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not allowed with this key.', $completeHeader['alg']));
176
        }
177
178
        $signatureAlgorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
179
        if (!$signatureAlgorithm instanceof SignatureAlgorithmInterface) {
180
            throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $completeHeader['alg']));
181
        }
182
183
        return $signatureAlgorithm;
184
    }
185
}
186