Passed
Push — master ( 4c571a...c0376f )
by Thomas
02:30
created

CertificateDetails::fromCertificate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.5

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 6
ccs 2
cts 4
cp 0.5
crap 2.5
rs 10
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