Skip to content

Commit 4379a59

Browse files
SpomkytempStephan Wentz
authored
Bugs/ed keys loading (#365)
* Add missing context parameters for supportsDenormalization() (#359) Co-authored-by: Stephan Wentz <swentz@brainbits.net> * Rebase * PKCS#8 Loading * PHPStan errors fixed * foo.key removed Co-authored-by: Stephan Wentz <stephan@wentz.it> Co-authored-by: Stephan Wentz <swentz@brainbits.net>
1 parent daf3440 commit 4379a59

File tree

8 files changed

+318
-18
lines changed

8 files changed

+318
-18
lines changed

src/Bundle/JoseFramework/Serializer/JWESerializer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function __construct(
2626
$this->serializerManager = $serializerManager;
2727
}
2828

29-
public function supportsDenormalization(mixed $data, string $type, string $format = null): bool
29+
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
3030
{
3131
return $type === JWE::class
3232
&& class_exists(JWESerializerManager::class)

src/Bundle/JoseFramework/Serializer/JWSSerializer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function __construct(
2626
$this->serializerManager = $serializerManager;
2727
}
2828

29-
public function supportsDenormalization(mixed $data, string $type, string $format = null): bool
29+
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
3030
{
3131
return $type === JWS::class
3232
&& class_exists(JWSSerializerManager::class)

src/Component/KeyManagement/KeyConverter/ECKey.php

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private static function loadPEM(string $data): array
6464
}
6565
$children = $asnObject->getChildren();
6666
if (self::isPKCS8($children)) {
67-
$children = self::loadPKCS8($children);
67+
return self::loadPKCS8($children);
6868
}
6969

7070
if (count($children) === 4) {
@@ -82,6 +82,19 @@ private static function loadPEM(string $data): array
8282
*/
8383
private static function loadPKCS8(array $children): array
8484
{
85+
$oidList = $children[1]->getContent();
86+
if (! is_array($oidList) || count($oidList) !== 2) {
87+
throw new InvalidArgumentException('Unable to load the key.');
88+
}
89+
$oid = $oidList[1];
90+
if (! $oid instanceof ObjectIdentifier) {
91+
throw new InvalidArgumentException('Unable to load the key.');
92+
}
93+
$oid = $oid->getContent();
94+
if (! is_string($oid)) {
95+
throw new InvalidArgumentException('Unable to load the key.');
96+
}
97+
8598
$data = $children[2]->getContent();
8699
if (! is_string($data)) {
87100
throw new InvalidArgumentException('Unable to load the key.');
@@ -91,8 +104,72 @@ private static function loadPKCS8(array $children): array
91104
if (! $asnObject instanceof Sequence) {
92105
throw new InvalidArgumentException('Unable to load the key.');
93106
}
107+
if ($asnObject->count() < 2) {
108+
throw new InvalidArgumentException('Unable to load the key.');
109+
}
110+
$version = $asnObject->getChildren()[0];
111+
if (! $version instanceof Integer && $version->getContent() !== '1') {
112+
throw new InvalidArgumentException('Unable to load the key.');
113+
}
114+
$privateKey = $asnObject->getChildren()[1];
115+
if (! $privateKey instanceof OctetString) {
116+
throw new InvalidArgumentException('Unable to load the key.');
117+
}
118+
$privateKey = $privateKey->getContent();
119+
if (! is_string($privateKey)) {
120+
throw new InvalidArgumentException('Unable to load the key.');
121+
}
122+
$dBin = hex2bin($privateKey);
123+
if (! is_string($dBin)) {
124+
throw new InvalidArgumentException('Unable to load the key.');
125+
}
126+
127+
$attributes = $asnObject->getChildren();
128+
$publicKeys = array_reduce($attributes, static function (array $carry, mixed $attribute): array {
129+
if (! $attribute instanceof ExplicitlyTaggedObject) {
130+
return $carry;
131+
}
132+
$attribute = $attribute->getContent();
133+
if (! is_array($attribute) || count($attribute) === 0) {
134+
return $carry;
135+
}
136+
$value = $attribute[0];
137+
if ($value instanceof BitString) {
138+
$carry[] = $value;
139+
}
140+
return $carry;
141+
}, []);
142+
143+
if (count($publicKeys) !== 1) {
144+
throw new InvalidArgumentException('Unable to load the key.');
145+
}
146+
$publicKey = $publicKeys[0];
147+
148+
if (! $publicKey instanceof BitString) {
149+
throw new InvalidArgumentException('Unable to load the key.');
150+
}
151+
$bits = $publicKey->getContent();
152+
if (! is_string($bits)) {
153+
throw new InvalidArgumentException('Unsupported key type');
154+
}
155+
$bits_length = mb_strlen($bits, '8bit');
156+
if (mb_strpos($bits, '04', 0, '8bit') !== 0) {
157+
throw new InvalidArgumentException('Unsupported key type');
158+
}
159+
160+
$xBin = hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit'));
161+
$yBin = hex2bin(mb_substr($bits, (int) (($bits_length - 2) / 2 + 2), ($bits_length - 2) / 2, '8bit'));
162+
if (! is_string($xBin) || ! is_string($yBin)) {
163+
throw new InvalidArgumentException('Unable to load the key.');
164+
}
94165

95-
return $asnObject->getChildren();
166+
return [
167+
'kty' => 'EC',
168+
'crv' => self::getCurve($oid),
169+
'x' => Base64UrlSafe::encodeUnpadded($xBin),
170+
'y' => Base64UrlSafe::encodeUnpadded($yBin),
171+
'd' => Base64UrlSafe::encodeUnpadded($dBin),
172+
];
96173
}
97174

98175
private static function loadPublicPEM(array $children): array

src/Component/KeyManagement/KeyConverter/KeyConverter.php

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
use function array_key_exists;
88
use function count;
99
use function extension_loaded;
10+
use FG\ASN1\Universal\BitString;
11+
use FG\ASN1\Universal\Integer;
12+
use FG\ASN1\Universal\ObjectIdentifier;
13+
use FG\ASN1\Universal\OctetString;
14+
use FG\ASN1\Universal\Sequence;
1015
use InvalidArgumentException;
1116
use function is_array;
1217
use function is_string;
1318
use const OPENSSL_KEYTYPE_EC;
1419
use const OPENSSL_KEYTYPE_RSA;
1520
use const OPENSSL_RAW_DATA;
1621
use OpenSSLCertificate;
22+
use ParagonIE\ConstantTime\Base64;
1723
use ParagonIE\ConstantTime\Base64UrlSafe;
1824
use const PHP_EOL;
1925
use const PREG_PATTERN_ORDER;
@@ -185,20 +191,131 @@ private static function loadKeyFromPEM(string $pem, ?string $password = null): a
185191
throw new InvalidArgumentException('Unable to get details of the key');
186192
}
187193

188-
switch ($details['type']) {
189-
case OPENSSL_KEYTYPE_EC:
190-
$ec_key = ECKey::createFromPEM($pem);
194+
return match ($details['type']) {
195+
OPENSSL_KEYTYPE_EC => ECKey::createFromPEM($pem)->toArray(),
196+
OPENSSL_KEYTYPE_RSA => RSAKey::createFromPEM($pem)->toArray(),
197+
-1 => self::tryToLoadOtherKeyTypes($pem),
198+
default => throw new InvalidArgumentException('Unsupported key type'),
199+
};
200+
}
191201

192-
return $ec_key->toArray();
202+
/**
203+
* This method tries to load Ed448, X488, Ed25519 and X25519 keys.
204+
*/
205+
private static function tryToLoadOtherKeyTypes(string $pem): array
206+
{
207+
try {
208+
preg_match_all('#(-.*-)#', $pem, $matches, PREG_PATTERN_ORDER);
209+
$data = preg_replace('#-.*-|\r|\n| #', '', $pem);
210+
if (! is_string($data)) {
211+
throw new InvalidArgumentException('Unsupported key type');
212+
}
213+
$der = Base64::decode($data);
214+
$sequence = Sequence::fromBinary($der);
215+
if (! $sequence instanceof Sequence) {
216+
throw new InvalidArgumentException('Unsupported key type');
217+
}
193218

194-
case OPENSSL_KEYTYPE_RSA:
195-
$rsa_key = RSAKey::createFromPEM($pem);
219+
return match ($sequence->count()) {
220+
2 => self::tryToLoadPublicKeyTypes($sequence),
221+
3 => self::tryToLoadPrivateKeyTypes($sequence),
222+
default => throw new InvalidArgumentException('Unsupported key type'),
223+
};
224+
} catch (Throwable $e) {
225+
throw new InvalidArgumentException('Unsupported key type', 0, $e);
226+
}
227+
}
196228

197-
return $rsa_key->toArray();
229+
/**
230+
* This method tries to load Ed448 or Ed25519 keys.
231+
*/
232+
private static function tryToLoadPublicKeyTypes(Sequence $sequence): array
233+
{
234+
[$curveId, $x] = $sequence;
235+
if (! $curveId instanceof Sequence || $curveId->count() === 0) {
236+
throw new InvalidArgumentException('Unsupported key type');
237+
}
238+
if (! $x instanceof BitString) {
239+
throw new InvalidArgumentException('Unsupported key type');
240+
}
241+
$oid = $curveId[0];
242+
if (! $oid instanceof ObjectIdentifier) {
243+
throw new InvalidArgumentException('Unsupported key type');
244+
}
245+
$curve = $oid->getContent();
246+
if (! is_string($curve)) {
247+
throw new InvalidArgumentException('Unsupported key type');
248+
}
198249

199-
default:
200-
throw new InvalidArgumentException('Unsupported key type');
250+
return [
251+
'kty' => 'OKP',
252+
'crv' => self::getCurve($curve),
253+
'x' => Base64UrlSafe::encodeUnpadded($x->getBinaryContent()),
254+
];
255+
}
256+
257+
/**
258+
* This method tries to load X448 or X25519 keys.
259+
*/
260+
private static function tryToLoadPrivateKeyTypes(Sequence $sequence): array
261+
{
262+
[$version, $curveId, $octetD] = $sequence;
263+
if ($version instanceof Integer && $version->getContent() !== '0') {
264+
throw new InvalidArgumentException('Unsupported key type');
265+
}
266+
if (! $curveId instanceof Sequence || $curveId->count() === 0) {
267+
throw new InvalidArgumentException('Unsupported key type');
268+
}
269+
if (! $octetD instanceof OctetString) {
270+
throw new InvalidArgumentException('Unsupported key type');
271+
}
272+
$oid = $curveId[0];
273+
if (! $oid instanceof ObjectIdentifier) {
274+
throw new InvalidArgumentException('Unsupported key type');
275+
}
276+
$curve = $oid->getContent();
277+
if (! is_string($curve)) {
278+
throw new InvalidArgumentException('Unsupported key type');
279+
}
280+
$crv = self::getCurve($curve);
281+
$binOctetdD = $octetD->getBinaryContent();
282+
$d = OctetString::fromBinary($binOctetdD);
283+
$d = $d->getContent();
284+
if (! is_string($d)) {
285+
throw new InvalidArgumentException('Unsupported key type');
286+
}
287+
$dBin = hex2bin($d);
288+
if (! is_string($dBin)) {
289+
throw new InvalidArgumentException('Unsupported key type');
201290
}
291+
292+
$data = [
293+
'kty' => 'OKP',
294+
'crv' => $crv,
295+
'd' => Base64UrlSafe::encodeUnpadded($dBin),
296+
];
297+
298+
if (($crv === 'Ed25519' || $crv === 'X25519') && extension_loaded('sodium')) {
299+
$data['x'] = Base64UrlSafe::encodeUnpadded(sodium_crypto_sign_publickey_from_secretkey($d));
300+
}
301+
302+
return $data;
303+
}
304+
305+
/**
306+
* This method modifies the PEM to get 64 char lines and fix bug with old OpenSSL versions.
307+
*/
308+
private static function getCurve(string $oid): string
309+
{
310+
return match ($oid) {
311+
'1.3.101.115' => 'Ed448ph',
312+
'1.3.101.114' => 'Ed25519ph',
313+
'1.3.101.113' => 'Ed448',
314+
'1.3.101.112' => 'Ed25519',
315+
'1.3.101.111' => 'X448',
316+
'1.3.101.110' => 'X25519',
317+
default => throw new InvalidArgumentException('Unsupported key type.'),
318+
};
202319
}
203320

204321
/**

src/SignatureAlgorithm/EdDSA/EdDSA.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@ public function sign(JWK $key, string $input): string
3232
if (! $key->has('d')) {
3333
throw new InvalidArgumentException('The EC key is not private');
3434
}
35-
$x = $key->get('x');
36-
if (! is_string($x)) {
37-
throw new InvalidArgumentException('Invalid "x" parameter.');
38-
}
3935
$d = $key->get('d');
4036
if (! is_string($d)) {
4137
throw new InvalidArgumentException('Invalid "d" parameter.');
4238
}
39+
if (! $key->has('x')) {
40+
$x = sodium_crypto_sign_publickey_from_secretkey($d);
41+
} else {
42+
$x = $key->get('x');
43+
}
44+
if (! is_string($x)) {
45+
throw new InvalidArgumentException('Invalid "x" parameter.');
46+
}
4347
$x = Base64UrlSafe::decode($x);
4448
$d = Base64UrlSafe::decode($d);
4549
$secret = $d . $x;

0 commit comments

Comments
 (0)