1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace MadWizard\WebAuthn\Pki; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use LogicException; |
7
|
|
|
use MadWizard\WebAuthn\Crypto\CoseAlgorithm; |
8
|
|
|
use MadWizard\WebAuthn\Exception\ParseException; |
9
|
|
|
use MadWizard\WebAuthn\Exception\WebAuthnException; |
10
|
|
|
use MadWizard\WebAuthn\Format\ByteBuffer; |
11
|
|
|
use Sop\ASN1\Element; |
12
|
|
|
use Sop\CryptoBridge\Crypto; |
13
|
|
|
use Sop\CryptoEncoding\PEM; |
14
|
|
|
use Sop\CryptoTypes\AlgorithmIdentifier\Feature\SignatureAlgorithmIdentifier; |
15
|
|
|
use Sop\CryptoTypes\AlgorithmIdentifier\Signature\ECDSAWithSHA256AlgorithmIdentifier; |
16
|
|
|
use Sop\CryptoTypes\AlgorithmIdentifier\Signature\SHA1WithRSAEncryptionAlgorithmIdentifier; |
17
|
|
|
use Sop\CryptoTypes\AlgorithmIdentifier\Signature\SHA256WithRSAEncryptionAlgorithmIdentifier; |
18
|
|
|
use Sop\CryptoTypes\Signature\Signature; |
19
|
|
|
use Sop\X509\Certificate\Certificate; |
20
|
|
|
use Sop\X509\Certificate\TBSCertificate; |
21
|
|
|
|
22
|
|
|
class CertificateDetails implements CertificateDetailsInterface |
23
|
|
|
{ |
24
|
|
|
public const VERSION_1 = TBSCertificate::VERSION_1; |
25
|
|
|
|
26
|
|
|
public const VERSION_2 = TBSCertificate::VERSION_2; |
27
|
|
|
|
28
|
|
|
public const VERSION_3 = TBSCertificate::VERSION_3; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var TBSCertificate |
32
|
|
|
*/ |
33
|
|
|
private $cert; |
34
|
|
|
|
35
|
18 |
|
private function __construct(TBSCertificate $certificate) |
36
|
|
|
{ |
37
|
18 |
|
$this->cert = $certificate; |
38
|
18 |
|
} |
39
|
|
|
|
40
|
17 |
|
public static function fromPem(string $pem): CertificateDetails |
41
|
|
|
{ |
42
|
|
|
try { |
43
|
17 |
|
return new self(Certificate::fromPEM(PEM::fromString($pem))->tbsCertificate()); |
44
|
1 |
|
} catch (Exception $e) { |
45
|
1 |
|
throw new ParseException('Failed to parse PEM certificate.', 0, $e); |
46
|
|
|
} |
47
|
|
|
} |
48
|
|
|
|
49
|
2 |
|
public static function fromCertificate(X509Certificate $certificate): CertificateDetails |
50
|
|
|
{ |
51
|
|
|
try { |
52
|
2 |
|
return new self(Certificate::fromDER($certificate->asDer())->tbsCertificate()); |
53
|
|
|
} catch (Exception $e) { |
54
|
|
|
throw new ParseException('Failed to parse PEM certificate.', 0, $e); |
55
|
|
|
} |
56
|
|
|
} |
57
|
|
|
|
58
|
7 |
|
public function verifySignature(string $data, string $signature, int $coseAlgorithm): bool |
59
|
|
|
{ |
60
|
7 |
|
$signatureAlgorithm = $this->convertCoseAlgorthm($coseAlgorithm); |
61
|
|
|
try { |
62
|
6 |
|
$signatureData = Signature::fromSignatureData($signature, $signatureAlgorithm); |
63
|
5 |
|
$key = $this->cert->subjectPublicKeyInfo(); |
64
|
5 |
|
$crypto = Crypto::getDefault(); |
65
|
5 |
|
return $crypto->verify($data, $signatureData, $key, $signatureAlgorithm); |
66
|
1 |
|
} catch (Exception $e) { |
67
|
1 |
|
throw new WebAuthnException('Failed to verify signature.', 0, $e); |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
1 |
|
public function getPublicKeyDer(): string |
72
|
|
|
{ |
73
|
|
|
try { |
74
|
1 |
|
return $this->cert->subjectPublicKeyInfo()->toDER(); |
75
|
|
|
} catch (Exception $e) { |
76
|
|
|
throw new ParseException('Failed to get public key from certificate.', 0, $e); |
77
|
|
|
} |
78
|
|
|
} |
79
|
|
|
|
80
|
7 |
|
private function convertCoseAlgorthm(int $coseAlgorithm): SignatureAlgorithmIdentifier |
81
|
|
|
{ |
82
|
7 |
|
switch ($coseAlgorithm) { |
83
|
|
|
case CoseAlgorithm::ES256: |
84
|
|
|
case CoseAlgorithm::ES384: |
85
|
|
|
case CoseAlgorithm::ES512: |
86
|
4 |
|
return new ECDSAWithSHA256AlgorithmIdentifier(); |
87
|
|
|
case CoseAlgorithm::RS256: |
88
|
|
|
case CoseAlgorithm::RS384: |
89
|
|
|
case CoseAlgorithm::RS512: |
90
|
1 |
|
return new SHA256WithRSAEncryptionAlgorithmIdentifier(); |
91
|
|
|
case CoseAlgorithm::RS1: |
92
|
1 |
|
return new SHA1WithRSAEncryptionAlgorithmIdentifier(); |
93
|
|
|
} |
94
|
|
|
|
95
|
1 |
|
throw new WebAuthnException(sprintf('Signature format %d not supported.', $coseAlgorithm)); |
96
|
|
|
} |
97
|
|
|
|
98
|
8 |
|
public function getExtensionData(string $oid): ?CertificateExtension |
99
|
|
|
{ |
100
|
|
|
try { |
101
|
8 |
|
$extension = $this->cert->extensions()->get($oid); |
102
|
2 |
|
} catch (LogicException $e) { |
103
|
|
|
// No extension present |
104
|
2 |
|
return null; |
105
|
|
|
} |
106
|
|
|
try { |
107
|
6 |
|
$seq = $extension->toASN1(); |
108
|
6 |
|
$idx = $seq->has(1, Element::TYPE_OCTET_STRING) ? 1 : 2; |
109
|
6 |
|
$der = $seq->at($idx)->asOctetString()->string(); |
110
|
6 |
|
return new CertificateExtension($extension->oid(), $extension->isCritical(), new ByteBuffer($der)); |
111
|
|
|
} catch (Exception $e) { |
112
|
|
|
throw new ParseException(sprintf('Failed to parse extension %s.', $oid), 0, $e); |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
5 |
|
public function getCertificateVersion(): ?int |
117
|
|
|
{ |
118
|
|
|
// NOTE: version() can throw a LogicException if no version is set, however this is never the case |
119
|
|
|
// when reading certificates. Even version 1 x509 certificates without the (optional) tagged version |
120
|
|
|
// will always default to version 1. |
121
|
5 |
|
return $this->cert->version(); |
122
|
|
|
} |
123
|
|
|
|
124
|
4 |
|
public function getOrganizationalUnit(): string |
125
|
|
|
{ |
126
|
|
|
try { |
127
|
4 |
|
return $this->cert->subject()->firstValueOf('OU')->stringValue(); |
128
|
1 |
|
} catch (Exception $e) { |
129
|
1 |
|
throw new ParseException('Failed to retrieve the organizational unit', 0, $e); |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
1 |
|
public function getSubject(): string |
134
|
|
|
{ |
135
|
|
|
try { |
136
|
1 |
|
return $this->cert->subject()->toString(); |
137
|
|
|
} catch (Exception $e) { |
138
|
|
|
throw new ParseException('Failed to retrieve subject unit', 0, $e); |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
2 |
|
public function getSubjectCommonName(): string |
143
|
|
|
{ |
144
|
|
|
try { |
145
|
2 |
|
return $this->cert->subject()->firstValueOf('CN')->stringValue(); |
146
|
|
|
} catch (Exception $e) { |
147
|
|
|
throw new ParseException('Failed to retrieve subject CN value', 0, $e); |
148
|
|
|
} |
149
|
|
|
} |
150
|
|
|
|
151
|
1 |
|
public function getSubjectAlternateNameDN(string $oid): string |
152
|
|
|
{ |
153
|
|
|
try { |
154
|
1 |
|
$attrValue = $this->cert->extensions()->subjectAlternativeName()->names()->firstDN()->firstValueOf($oid); |
155
|
1 |
|
return $attrValue->toASN1()->asUnspecified()->asUTF8String()->string(); |
156
|
|
|
} catch (Exception $e) { |
157
|
|
|
throw new ParseException(sprintf('Failed to retrieve %s entry in directoryName in alternate name.', $oid), 0, $e); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
5 |
|
public function isCA(): ?bool |
162
|
|
|
{ |
163
|
5 |
|
$extensions = $this->cert->extensions(); |
164
|
|
|
|
165
|
5 |
|
if (!$extensions->hasBasicConstraints()) { |
166
|
1 |
|
return null; |
167
|
|
|
} |
168
|
|
|
|
169
|
4 |
|
return $extensions->basicConstraints()->isCA(); |
170
|
|
|
} |
171
|
|
|
|
172
|
1 |
|
public function extendedKeyUsageContains(string $oid): bool |
173
|
|
|
{ |
174
|
|
|
try { |
175
|
1 |
|
$extensions = $this->cert->extensions(); |
176
|
1 |
|
if (!$extensions->hasExtendedKeyUsage()) { |
177
|
|
|
return false; |
178
|
|
|
} |
179
|
1 |
|
return $extensions->extendedKeyUsage()->has($oid); |
180
|
|
|
} catch (Exception $e) { |
181
|
|
|
throw new ParseException('Failed to retrieve subject unit', 0, $e); |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
public function getPublicKeyIdentifier(): string |
186
|
|
|
{ |
187
|
|
|
try { |
188
|
|
|
return \bin2hex($this->cert->subjectPublicKeyInfo()->keyIdentifier()); |
189
|
|
|
} catch (Exception $e) { |
190
|
|
|
throw new ParseException('Failed to get public key identifier', 0, $e); |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
} |
194
|
|
|
|