diff --git a/README.md b/README.md index e375d52..62a38d4 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,25 @@ [![StyleCI](https://styleci.io/repos/64165510/shield)](https://styleci.io/repos/64165510) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/ssl-certificate.svg?style=flat-square)](https://packagist.org/packages/spatie/ssl-certificate) -The class provided by this package makes it incredibly easy to query the properties on an ssl certificate. Here's an example: - +The class provided by this package makes it incredibly easy to query the properties on an ssl certificate. We have three options for fetching a certficate. Here's an example: ```php use Spatie\SslCertificate\SslCertificate; +// fetch the certificate using an url $certificate = SslCertificate::createForHostName('spatie.be'); +// or from a certificate file +$certificate = SslCertificate::createFromFile($pathToCertificateFile); + +// or from a string +$certificate = SslCertificate::createFromString($certificateData); + $certificate->getIssuer(); // returns "Let's Encrypt Authority X3" $certificate->isValid(); // returns true if the certificate is currently valid $certificate->expirationDate(); // returns an instance of Carbon $certificate->expirationDate()->diffInDays(); // returns an int $certificate->getSignatureAlgorithm(); // returns a string +$certificate->getOrganization(); // returns the organization name when available ``` Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). @@ -86,6 +93,14 @@ Returns the algorithm used for signing the certificate $certificate->getSignatureAlgorithm(); // returns "RSA-SHA256" ``` +### Getting the certificate's organization + +Returns the organization belonging to the certificate + +```php +$certificate->getOrganization(); // returns "Spatie BVBA" +``` + ### Getting the additional domain names A certificate can cover multiple (sub)domains. Here's how to get them. @@ -120,8 +135,6 @@ $certificate->validFromDate(); // returns an instance of Carbon $certificate->expirationDate(); // returns an instance of Carbon ``` - - ### Determining if the certificate is still valid Returns true if the current Date and time is between `validFromDate` and `expirationDate`. diff --git a/src/SslCertificate.php b/src/SslCertificate.php index df555fa..9e89c56 100644 --- a/src/SslCertificate.php +++ b/src/SslCertificate.php @@ -31,6 +31,25 @@ public static function createForHostName(string $url, int $timeout = 30): self return Downloader::downloadCertificateFromUrl($url, $timeout); } + public static function createFromFile(string $pathToCertificate): self + { + return $this->createFromString(file_get_contents($pathToCertificate)); + } + + public static function createFromString(string $certificatePem): self + { + $certificateFields = openssl_x509_parse($certificatePem); + + $fingerprint = openssl_x509_fingerprint($certificatePem); + $fingerprintSha256 = openssl_x509_fingerprint($certificatePem, 'sha256'); + + return new self( + $certificateFields, + $fingerprint, + $fingerprintSha256 + ); + } + public function __construct( array $rawCertificateFields, string $fingerprint = '', @@ -78,6 +97,11 @@ public function getSignatureAlgorithm(): string return $this->rawCertificateFields['signatureTypeSN'] ?? ''; } + public function getOrganization(): string + { + return $this->rawCertificateFields['subject']['O'] ?? ''; + } + public function getFingerprint(): string { return $this->fingerprint; diff --git a/tests/SslCertificateFromStringTest.php b/tests/SslCertificateFromStringTest.php new file mode 100644 index 0000000..833720c --- /dev/null +++ b/tests/SslCertificateFromStringTest.php @@ -0,0 +1,216 @@ +certificate = SslCertificate::createFromString($certificate); + } + + /** @test */ + public function it_can_determine_the_issuer() + { + $this->assertSame("Let's Encrypt Authority X3", $this->certificate->getIssuer()); + } + + /** @test */ + public function it_can_determine_the_domain() + { + $this->assertSame('analytics.spatie.be', $this->certificate->getDomain()); + } + + /** @test */ + public function it_can_determine_the_signature_algorithm() + { + $this->assertSame('RSA-SHA256', $this->certificate->getSignatureAlgorithm()); + } + + /** @test */ + public function it_can_determine_the_additional_domains() + { + $this->assertCount(1, $this->certificate->getAdditionalDomains()); + + $this->assertSame('analytics.spatie.be', $this->certificate->getAdditionalDomains()[0]); + } + + /** @test */ + public function it_can_determine_the_valid_from_date() + { + $this->assertInstanceOf(Carbon::class, $this->certificate->validFromDate()); + + $this->assertSame('2020-01-13 03:18:13', $this->certificate->validFromDate()->format('Y-m-d H:i:s')); + } + + /** @test */ + public function it_can_determine_the_expiration_date() + { + $this->assertInstanceOf(Carbon::class, $this->certificate->expirationDate()); + + $this->assertSame('2020-04-12 03:18:13', $this->certificate->expirationDate()->format('Y-m-d H:i:s')); + } + + /** @test */ + public function it_can_determine_if_the_certificate_is_valid() + { + Carbon::setTestNow(Carbon::create('2016', '05', '19', '16', '45', '00', 'utc')); + $this->assertFalse($this->certificate->isValid()); + + Carbon::setTestNow(Carbon::create('2020', '02', '13', '16', '51', '00', 'utc')); + $this->assertTrue($this->certificate->isValid()); + + Carbon::setTestNow(Carbon::create('2020', '03', '17', '16', '49', '00', 'utc')); + $this->assertTrue($this->certificate->isValid()); + + Carbon::setTestNow(Carbon::create('2016', '08', '17', '16', '51', '00', 'utc')); + $this->assertFalse($this->certificate->isValid()); + } + + /** @test */ + public function it_can_determine_if_the_certificate_is_expired() + { + Carbon::setTestNow(Carbon::create('2020', '02', '13', '16', '45', '00', 'utc')); + $this->assertFalse($this->certificate->isExpired()); + + Carbon::setTestNow(Carbon::create('2020', '02', '13', '16', '51', '00', 'utc')); + $this->assertFalse($this->certificate->isExpired()); + + Carbon::setTestNow(Carbon::create('2020', '02', '17', '16', '49', '00', 'utc')); + $this->assertFalse($this->certificate->isExpired()); + + Carbon::setTestNow(Carbon::create('2020', '08', '17', '16', '51', '00', 'utc')); + $this->assertTrue($this->certificate->isExpired()); + } + + /** @test */ + public function it_provides_a_fluent_interface_to_set_all_options() + { + $downloadedCertificate = SslCertificate::download() + ->usingPort(443) + ->setTimeout(30) + ->forHost('spatie.be'); + + $this->assertSame('spatie.be', $downloadedCertificate->getDomain()); + } + + /** @test */ + public function it_can_convert_the_certificate_to_json() + { + $this->assertMatchesJsonSnapshot($this->certificate->getRawCertificateFieldsJson()); + } + + /** @test */ + public function it_can_convert_the_certificate_to_a_string() + { + $this->assertEquals( + $this->certificate->getRawCertificateFieldsJson(), + (string) $this->certificate + ); + } + + /** @test */ + public function it_can_get_the_hash_of_a_certificate() + { + $this->assertEquals('0547c1a78dcdbe96f907aaaf42db5b8f', $this->certificate->getHash()); + } + + /** @test */ + public function it_can_get_all_domains() + { + $this->assertEquals([ + 0 => 'analytics.spatie.be', + ], $this->certificate->getDomains()); + } + + /** @test */ + public function it_can_get_the_days_until_the_expiration_date() + { + $this->assertEquals(90, $this->certificate->daysUntilExpirationDate()); + } + + /** @test */ + public function it_can_determine_if_it_is_self_signed() + { + $this->assertFalse($this->certificate->isSelfSigned()); + } + + /** @test */ + public function it_can_determine_if_it_uses_sha1_hasing() + { + $this->assertFalse($this->certificate->usesSha1Hash()); + } + + /** @test */ + public function it_can_determine_if_the_certificate_has_a_certain_domain() + { + $this->assertTrue($this->certificate->containsDomain('analytics.spatie.be')); + + $this->assertFalse($this->certificate->containsDomain('www.example.com')); + $this->assertFalse($this->certificate->containsDomain('notreallyspatie.be')); + $this->assertFalse($this->certificate->containsDomain('spatie.be.example.com')); + } + + /** @test */ + public function does_not_notify_on_wrong_domains() + { + $rawCertificateFields = json_decode( + file_get_contents(__DIR__.'/stubs/certificateWithRandomWildcardDomains.json'), + true + ); + + $this->certificate = new SslCertificate($rawCertificateFields); + + $this->assertFalse($this->certificate->appliesToUrl('https://coinmarketcap.com')); + } + + /** @test */ + public function it_correctly_compares_uppercase_domain_names() + { + $rawCertificateFields = json_decode( + file_get_contents(__DIR__.'/stubs/certificateWithUppercaseDomains.json'), + true + ); + + $this->certificate = new SslCertificate($rawCertificateFields); + + $this->assertTrue($this->certificate->appliesToUrl('spatie.be')); + $this->assertTrue($this->certificate->appliesToUrl('www.spatie.be')); + } + + /** @test */ + public function it_correctly_identifies_pre_certificates() + { + $rawCertificateFieldsNormalCertificate = json_decode( + file_get_contents(__DIR__.'/stubs/spatieCertificateFields.json'), + true + ); + + $rawCertificateFieldsPreCertificate = json_decode( + file_get_contents(__DIR__.'/stubs/preCertificate.json'), + true + ); + + $certificateNormal = new SslCertificate($rawCertificateFieldsNormalCertificate); + $certificatePreCertificate = new SslCertificate($rawCertificateFieldsPreCertificate); + + $this->assertFalse($certificateNormal->isPreCertificate()); + $this->assertTrue($certificatePreCertificate->isPreCertificate()); + } +} diff --git a/tests/SslCertificateTest.php b/tests/SslCertificateTest.php index e703839..e167bdc 100644 --- a/tests/SslCertificateTest.php +++ b/tests/SslCertificateTest.php @@ -12,7 +12,7 @@ class SslCertificateTest extends TestCase { use MatchesSnapshots; - /** @var SslCertificate */ + /** @var Spatie\SslCertificate\SslCertificate */ protected $certificate; public function setUp(): void @@ -26,7 +26,7 @@ public function setUp(): void $this->certificate = new SslCertificate($rawCertificateFields); } - public function it_can_get_the_raw_certificate_fiels() + public function it_can_get_the_raw_certificate_fields() { $rawCertificateFields = $this->certificate->getRawCertificateFields(); diff --git a/tests/__snapshots__/SslCertificateFromStringTest__it_can_convert_the_certificate_to_json__1.json b/tests/__snapshots__/SslCertificateFromStringTest__it_can_convert_the_certificate_to_json__1.json new file mode 100644 index 0000000..26d4d82 --- /dev/null +++ b/tests/__snapshots__/SslCertificateFromStringTest__it_can_convert_the_certificate_to_json__1.json @@ -0,0 +1,80 @@ +{ + "name": "\/CN=analytics.spatie.be", + "subject": { + "CN": "analytics.spatie.be" + }, + "hash": "18d9542d", + "issuer": { + "C": "US", + "O": "Let's Encrypt", + "CN": "Let's Encrypt Authority X3" + }, + "version": 2, + "serialNumber": "0x03DE0F2C502C8A71EC30E56708B6C44E9FDA", + "serialNumberHex": "03DE0F2C502C8A71EC30E56708B6C44E9FDA", + "validFrom": "200113031813Z", + "validTo": "200412031813Z", + "validFrom_time_t": 1578885493, + "validTo_time_t": 1586661493, + "signatureTypeSN": "RSA-SHA256", + "signatureTypeLN": "sha256WithRSAEncryption", + "signatureTypeNID": 668, + "purposes": { + "1": [ + true, + false, + "sslclient" + ], + "2": [ + true, + false, + "sslserver" + ], + "3": [ + true, + false, + "nssslserver" + ], + "4": [ + false, + false, + "smimesign" + ], + "5": [ + false, + false, + "smimeencrypt" + ], + "6": [ + false, + false, + "crlsign" + ], + "7": [ + true, + true, + "any" + ], + "8": [ + true, + false, + "ocsphelper" + ], + "9": [ + false, + false, + "timestampsign" + ] + }, + "extensions": { + "keyUsage": "Digital Signature, Key Encipherment", + "extendedKeyUsage": "TLS Web Server Authentication, TLS Web Client Authentication", + "basicConstraints": "CA:FALSE", + "subjectKeyIdentifier": "84:93:3A:3C:73:0D:EA:A6:59:3E:79:21:80:96:FF:95:A1:B7:0F:5D", + "authorityKeyIdentifier": "keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n", + "authorityInfoAccess": "OCSP - URI:http:\/\/ocsp.int-x3.letsencrypt.org\nCA Issuers - URI:http:\/\/cert.int-x3.letsencrypt.org\/\n", + "subjectAltName": "DNS:analytics.spatie.be", + "certificatePolicies": "Policy: 2.23.140.1.2.1\nPolicy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http:\/\/cps.letsencrypt.org\n", + "ct_precert_scts": "Signed Certificate Timestamp:\n Version : v1 (0x0)\n Log ID : 5E:A7:73:F9:DF:56:C0:E7:B5:36:48:7D:D0:49:E0:32:\n 7A:91:9A:0C:84:A1:12:12:84:18:75:96:81:71:45:58\n Timestamp : Jan 13 04:18:13.691 2020 GMT\n Extensions: none\n Signature : ecdsa-with-SHA256\n 30:46:02:21:00:B8:E0:C8:73:90:2E:59:15:28:D2:7E:\n 5F:63:A7:14:BD:0E:A6:40:42:52:C5:20:B1:30:A1:21:\n 25:1F:C3:CB:D0:02:21:00:DA:4D:D5:EC:E5:28:F2:24:\n 79:1C:F3:02:B0:A0:C1:DD:9B:65:0F:95:19:52:9B:27:\n 68:DA:72:84:83:E9:B4:18\nSigned Certificate Timestamp:\n Version : v1 (0x0)\n Log ID : B2:1E:05:CC:8B:A2:CD:8A:20:4E:87:66:F9:2B:B9:8A:\n 25:20:67:6B:DA:FA:70:E7:B2:49:53:2D:EF:8B:90:5E\n Timestamp : Jan 13 04:18:13.670 2020 GMT\n Extensions: none\n Signature : ecdsa-with-SHA256\n 30:44:02:20:41:9C:70:D9:1F:60:9E:C9:65:53:E0:2F:\n B8:A7:FD:29:D7:33:FF:F8:73:BC:1F:D3:C2:1C:85:80:\n F2:70:10:3B:02:20:28:10:E5:4D:E3:90:C8:04:58:58:\n 51:88:B5:08:34:CF:CA:9D:52:1B:86:56:4C:04:3A:B3:\n 4E:D7:BE:3D:13:2E" + } +} diff --git a/tests/stubs/spatieCertificate.pem b/tests/stubs/spatieCertificate.pem new file mode 100644 index 0000000..cce4e3e --- /dev/null +++ b/tests/stubs/spatieCertificate.pem @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGXjCCBUagAwIBAgISA94PLFAsinHsMOVnCLbETp/aMA0GCSqGSIb3DQEBCwUA +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDAxMTMwMzE4MTNaFw0y +MDA0MTIwMzE4MTNaMB4xHDAaBgNVBAMTE2FuYWx5dGljcy5zcGF0aWUuYmUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC8dr/oGjkJhoeHCw6nEBO3rLsI +fWcVDLcwzpyg+McImr5ggX3btP0lYybH5RS6fAHPKL8IUJDJ98j07C92VncCLkmF +zgp3BgTlwQo/mtYQ8rkUY9ptgDQpxZhJuO9SrglEdd8AyaVfIPR667ygFOtMlxn7 +JfAVwxuQDzmYvurkB1M8vBI+jnV4F32fd0E80Q86Mj4kfpLeyqb/bPrnR2gR9/GI +DwIJP8KI1kggQ8IXRgd/BVJ//kErV4OrdZrbnhK/u+d1u8+QtbYDBSpcoha3ru0A +hdUUDUXRXfQZj1Nc7qUBU3IT6S1h2Yaj45Wuhf/0fIsk+p9P/sQzPHqMTgQEf1Uj +C1lHCzC4DtnEv0VPTylNRR2+/M2sBto7fRc0CK97NWeUjy1gjLQChCmkFwAmjZPi +nFJwpdEaZnnEfEAwMNxm9kn4YCxbk23iei72bqB/xrw33BN+GDdHVxYNf/uELyRW +L1fO8YXHm8x9vxbHBtAuTNe9bWE6cjch4i5KFigggw3XX5jMF3p0DJU9FyzuGvqX +op6sb/IA7fPkGajp270NwY/N8GmqNAzek2+SjrdZPuSbCCuXuSlhEU+LllVG40Ia +4rO+XrZx6g9vtwmaTiW11fvUstjmaID0xuoO2r43bOkQDKcPgXnEqG9rINJIjCTm +fviO5oq3NVZgC5VQ0QIDAQABo4ICaDCCAmQwDgYDVR0PAQH/BAQDAgWgMB0GA1Ud +JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW +BBSEkzo8cw3qplk+eSGAlv+VobcPXTAfBgNVHSMEGDAWgBSoSmpjBH3duubRObem +RWXv86jsoTBvBggrBgEFBQcBAQRjMGEwLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3Nw +LmludC14My5sZXRzZW5jcnlwdC5vcmcwLwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0 +LmludC14My5sZXRzZW5jcnlwdC5vcmcvMB4GA1UdEQQXMBWCE2FuYWx5dGljcy5z +cGF0aWUuYmUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAm +BggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEEBgorBgEE +AdZ5AgQCBIH1BIHyAPAAdwBep3P531bA57U2SH3QSeAyepGaDIShEhKEGHWWgXFF +WAAAAW+dH6I7AAAEAwBIMEYCIQC44MhzkC5ZFSjSfl9jpxS9DqZAQlLFILEwoSEl +H8PL0AIhANpN1ezlKPIkeRzzArCgwd2bZQ+VGVKbJ2jacoSD6bQYAHUAsh4FzIui +zYogTodm+Su5iiUgZ2va+nDnsklTLe+LkF4AAAFvnR+iJgAABAMARjBEAiBBnHDZ +H2CeyWVT4C+4p/0p1zP/+HO8H9PCHIWA8nAQOwIgKBDlTeOQyARYWFGItQg0z8qd +UhuGVkwEOrNO1749Ey4wDQYJKoZIhvcNAQELBQADggEBAHfGk/esxQNKLh0IH2b8 +FAhT2gJmf7/86hKaO0/6HoKv8i1wJu5scu+pFQuiCDs307A9Qe+fy8V0RdnK9cN5 +E3Ni22GppMQVYUXt48Lh2U1edatWnMdflVIGWODK71tnpvbEyK9F3yC7DnH3gERj +bTwpOVlCflqiGAEdW6T+cjARhICSAI5YOs6BF2lZLqRZ+1Hw4ySd2qN8HgDsPsQM +kJYA75Fx2bVENY26AgekxNUh7YRSI/CtSHjrYo6a/wsHt++pCyZ1VRx0pMKOY3nl +ADxogwwuqxcyE65KS7OTXMeQFsOaD95FZVS6ghnv2wa07BNAQ9PulWRP9BMfRnpu ++EM= +-----END CERTIFICATE-----