diff --git a/lib/Handler/CertificateEngine/OpenSslHandler.php b/lib/Handler/CertificateEngine/OpenSslHandler.php index 82c4da1f3d..da9d631ffa 100644 --- a/lib/Handler/CertificateEngine/OpenSslHandler.php +++ b/lib/Handler/CertificateEngine/OpenSslHandler.php @@ -48,7 +48,10 @@ public function generateRootCert( $csr = openssl_csr_new($this->getCsrNames(), $privateKey, ['digest_alg' => 'sha256']); $options = $this->getRootCertOptions(); - $x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365 * 5, $options); + + $serialNumber = random_int(1000000, 2147483647); + + $x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365 * 5, $options, $serialNumber); openssl_csr_export($csr, $csrout); openssl_x509_export($x509, $certout); @@ -94,12 +97,13 @@ public function generateCertificate(): string { throw new LibresignException('OpenSSL error: ' . $message); } + $serialNumber = random_int(1000000, 2147483647); + $x509 = openssl_csr_sign($csr, $rootCertificate, $rootPrivateKey, $this->expirity(), [ 'config' => $this->getFilenameToLeafCert(), - // This will set "basicConstraints" to CA:FALSE, the default is CA:TRUE - // The signer certificate is not a Certificate Authority 'x509_extensions' => 'v3_req', - ]); + ], $serialNumber); + return parent::exportToPkcs12( $x509, $privateKey, diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 4dcd803e34..314cfd9d86 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -68,6 +68,8 @@ * subject: string, * issuer: string, * extensions: string, + * serialNumber: string, + * serialNumberHex: string, * validate: array{ * from: string, * to: string, diff --git a/openapi-full.json b/openapi-full.json index 3571f1f6d2..d1f6fc416e 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -106,6 +106,8 @@ "subject", "issuer", "extensions", + "serialNumber", + "serialNumberHex", "validate" ], "properties": { @@ -121,6 +123,12 @@ "extensions": { "type": "string" }, + "serialNumber": { + "type": "string" + }, + "serialNumberHex": { + "type": "string" + }, "validate": { "type": "object", "required": [ diff --git a/openapi.json b/openapi.json index 86a6151698..f5a59bdf49 100644 --- a/openapi.json +++ b/openapi.json @@ -106,6 +106,8 @@ "subject", "issuer", "extensions", + "serialNumber", + "serialNumberHex", "validate" ], "properties": { @@ -121,6 +123,12 @@ "extensions": { "type": "string" }, + "serialNumber": { + "type": "string" + }, + "serialNumberHex": { + "type": "string" + }, "validate": { "type": "object", "required": [ diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index b0a437570f..45adf0b189 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1293,6 +1293,8 @@ export type components = { subject: string; issuer: string; extensions: string; + serialNumber: string; + serialNumberHex: string; validate: { from: string; to: string; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index dc3ba6feab..d72505e73c 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -980,6 +980,8 @@ export type components = { subject: string; issuer: string; extensions: string; + serialNumber: string; + serialNumberHex: string; validate: { from: string; to: string; diff --git a/src/views/ReadCertificate/CertificateContent.vue b/src/views/ReadCertificate/CertificateContent.vue index f9884d4fa3..45eae2524c 100644 --- a/src/views/ReadCertificate/CertificateContent.vue +++ b/src/views/ReadCertificate/CertificateContent.vue @@ -67,6 +67,10 @@ {{ t('libresign', 'Serial number') }} {{ certificate.serialNumber }} +
+ {{ t('libresign', 'Serial number (hex)') }} + {{ certificate.serialNumberHex }} +
diff --git a/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php b/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php index f810851b6e..3e67ccf741 100644 --- a/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php +++ b/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php @@ -245,4 +245,57 @@ public static function dataReadCertificate(): array { ], ]; } + + public function testSerialNumberGeneration(): void { + $rootInstance = $this->getInstance(); + $rootInstance->generateRootCert('', []); + + $signerInstance = $this->getInstance(); + $signerInstance->setCommonName('Test User'); + $signerInstance->setPassword('123456'); + + $certificate = $signerInstance->generateCertificate(); + $parsed = $signerInstance->readCertificate($certificate, '123456'); + + $this->assertArrayHasKey('serialNumber', $parsed, 'Certificate should have serialNumber field'); + $this->assertArrayHasKey('serialNumberHex', $parsed, 'Certificate should have serialNumberHex field'); + $this->assertNotNull($parsed['serialNumber'], 'Serial number should not be null'); + $this->assertNotNull($parsed['serialNumberHex'], 'Serial number hex should not be null'); + + $this->assertNotEquals('0', $parsed['serialNumber'], 'Serial number should not be zero'); + $this->assertNotEquals('00', $parsed['serialNumberHex'], 'Serial number hex should not be zero'); + + $serialInt = (int)$parsed['serialNumber']; + $this->assertGreaterThanOrEqual(1000000, $serialInt, 'Serial number should be >= 1000000'); + $this->assertLessThanOrEqual(2147483647, $serialInt, 'Serial number should be <= 2147483647'); + + $this->assertIsNumeric($parsed['serialNumber'], 'Serial number should be numeric'); + $this->assertMatchesRegularExpression('/^[0-9A-Fa-f]+$/', $parsed['serialNumberHex'], 'Serial number hex should contain only hex characters'); + } + + public function testUniqueSerialNumbers(): void { + $rootInstance = $this->getInstance(); + $rootInstance->generateRootCert('', []); + + $serialNumbers = []; + $numCertificates = 3; + + for ($i = 0; $i < $numCertificates; $i++) { + $signerInstance = $this->getInstance(); + $signerInstance->setCommonName("Test Certificate $i"); + $signerInstance->setPassword('123456'); + $certificateContent = $signerInstance->generateCertificate(); + $parsed = $signerInstance->readCertificate($certificateContent, '123456'); + + $serialNumber = $parsed['serialNumber']; + + $this->assertNotEquals('0', $serialNumber, "Certificate $i should not have serial number 0"); + + $this->assertNotContains($serialNumber, $serialNumbers, "Certificate $i should have unique serial number"); + + $serialNumbers[] = $serialNumber; + } + + $this->assertCount($numCertificates, array_unique($serialNumbers), 'All serial numbers should be unique'); + } }