Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.14% covered (warning)
56.14%
32 / 57
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
PublicKeyConverter
56.14% covered (warning)
56.14%
32 / 57
50.00% covered (danger)
50.00%
1 / 2
159.83
0.00% covered (danger)
0.00%
0 / 1
 fromCoseToPkcs
48.98% covered (danger)
48.98%
24 / 49
0.00% covered (danger)
0.00%
0 / 1
197.69
 publicKeyToPem
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * Public key converter for WebAuthn
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-02-24
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Auth\WebAuthn;
21
22use CBOR\CBOREncoder;
23use phpMyFAQ\Core\Exception;
24use phpseclib3\Crypt\PublicKeyLoader;
25use phpseclib3\Math\BigInteger;
26
27class PublicKeyConverter
28{
29    private const int ES256 = -7;
30    private const int RS256 = -257;
31
32    /**
33     * Convert COSE ECDHA to PKCS
34     *
35     * @throws Exception
36     * @throws \Exception
37     */
38    public static function fromCoseToPkcs(string $binary): ?string
39    {
40        $cosePubKey = CBOREncoder::decode($binary);
41
42        if (!is_array($cosePubKey)) {
43            return null;
44        }
45
46        if (!array_key_exists(3, $cosePubKey) || $cosePubKey[3] === null) { /* cose_alg */
47            return null;
48        }
49
50        switch ($cosePubKey[3]) {
51            case self::ES256:
52                /* COSE Alg: ECDSA w/ SHA-256 */
53                if (!array_key_exists(-1, $cosePubKey) || $cosePubKey[-1] === null) { /* cose_crv */
54                    throw new Exception('Cannot decode key response for curve');
55                }
56
57                if (!array_key_exists(-2, $cosePubKey) || $cosePubKey[-2] === null) { /* cose_crv_x */
58                    throw new Exception('Cannot decode key response for x coordinate');
59                }
60
61                if ($cosePubKey[-1] !== 1) { /* cose_crv_P256 */
62                    throw new Exception('Cannot decode key response for curve P256');
63                }
64
65                if (!array_key_exists(-2, $cosePubKey) || $cosePubKey[-2] === null) { /* cose_crv_x */
66                    throw new Exception('x coordinate for curve missing');
67                }
68
69                if (!array_key_exists(1, $cosePubKey) || $cosePubKey[1] === null) { /* cose_kty */
70                    throw new Exception('Cannot decode key response for key type');
71                }
72
73                if (!array_key_exists(-3, $cosePubKey) || $cosePubKey[-3] === null) { /* cose_crv_y */
74                    throw new Exception('Cannot decode key response for y coordinate');
75                }
76
77                if (!array_key_exists(-3, $cosePubKey) || $cosePubKey[-3] === null) { /* cose_crv_y */
78                    throw new Exception('y coordinate for curve missing');
79                }
80
81                if ($cosePubKey[1] !== 2) { /* cose_kty_ec2 */
82                    throw new Exception('Cannot decode key response for key type EC2');
83                }
84
85                $xEntry = $cosePubKey[-2];
86                $yEntry = $cosePubKey[-3];
87                if (
88                    !is_object($xEntry)
89                    || !method_exists($xEntry, 'get_byte_string')
90                    || !is_object($yEntry)
91                    || !method_exists($yEntry, 'get_byte_string')
92                ) {
93                    throw new Exception('Cannot decode key response for x or y coordinate');
94                }
95
96                $x = (string) $xEntry->get_byte_string();
97                $y = (string) $yEntry->get_byte_string();
98                if (strlen($x) !== 32 || strlen($y) !== 32) {
99                    throw new Exception('Cannot decode key response for x or y coordinate');
100                }
101
102                return self::publicKeyToPem("\x04" . $x . $y);
103            case self::RS256:
104                if (!array_key_exists(-2, $cosePubKey) || $cosePubKey[-2] === null) {
105                    throw new Exception('RSA Exponent missing');
106                }
107
108                if (!array_key_exists(-1, $cosePubKey) || $cosePubKey[-1] === null) {
109                    throw new Exception('RSA Modulus missing');
110                }
111
112                $exponentEntry = $cosePubKey[-2];
113                $modulusEntry = $cosePubKey[-1];
114                if (
115                    !is_object($exponentEntry)
116                    || !method_exists($exponentEntry, 'get_byte_string')
117                    || !is_object($modulusEntry)
118                    || !method_exists($modulusEntry, 'get_byte_string')
119                ) {
120                    throw new Exception('Cannot decode key response for RSA exponent or modulus');
121                }
122
123                $e = new BigInteger(bin2hex((string) $exponentEntry->get_byte_string()), 16);
124                $n = new BigInteger(bin2hex((string) $modulusEntry->get_byte_string()), 16);
125                return (string) PublicKeyLoader::load(['e' => $e, 'n' => $n]);
126            default:
127                return null;
128        }
129    }
130
131    private static function publicKeyToPem(string $key): ?string
132    {
133        if (strlen($key) !== 65 || $key[0] !== "\x04") {
134            return null;
135        }
136
137        $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
138        $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
139        $der .= "\x00" . $key;
140        $pem = "-----BEGIN PUBLIC KEY-----\x0A";
141        $pem .= chunk_split(string: base64_encode(string: $der), length: 64);
142
143        return $pem . "-----END PUBLIC KEY-----\x0A";
144    }
145}