Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions lib/Handler/CertificateEngine/OpenSslHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,7 @@ private function getRootCertOptions(string $configFile): array {
];
}

private function getLeafCertOptions(): array {
$configFile = $this->generateLeafConfig();

private function getLeafCertOptions(string $configFile): array {
return [
'digest_alg' => 'sha256',
'config' => $configFile,
Expand Down Expand Up @@ -179,7 +177,12 @@ public function generateCertificate(): string {
throw $this->buildOpenSslException('Failed to generate OpenSSL private key');
}

$csr = $this->createCsr($this->getCsrNames(), $privateKey, ['digest_alg' => 'sha256']);
$configFile = $this->generateLeafConfig();
$csr = $this->createCsr($this->getCsrNames(), $privateKey, [
'digest_alg' => 'sha256',
'config' => $configFile,
'config_section_name' => 'req',
]);
if ($csr === false) {
throw $this->buildOpenSslException('Failed to generate OpenSSL CSR');
}
Expand All @@ -202,7 +205,7 @@ public function generateCertificate(): string {
CertificateType::LEAF->value,
);
$serialNumber = (int)$serialNumberString;
$options = $this->getLeafCertOptions();
$options = $this->getLeafCertOptions($configFile);

$x509 = $this->signCsr($csr, $rootCertificate, $rootPrivateKey, $this->getLeafExpiryInDays(), $options, $serialNumber);
if ($x509 === false) {
Expand Down Expand Up @@ -357,7 +360,6 @@ private function buildLeafCertificateConfig(): array {
$config = [
'req' => [
'distinguished_name' => 'req_distinguished_name',
'req_extensions' => 'v3_req',
'prompt' => 'no',
],
'req_distinguished_name' => [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,111 @@ public function testSerialNumberGeneration(): void {
$this->assertMatchesRegularExpression('/^[0-9A-Fa-f]+$/', $parsed['serialNumberHex'], 'Serial number hex should contain only hex characters');
}

#[DataProvider('leafSubjectCases')]
public function testLeafCertificateSubjectMatchesConfiguredFields(array $configuredFields, array $expectedSubject): void {
$rootInstance = $this->getInstance();
$rootInstance->generateRootCert('Test Root CA', []);

$signerInstance = $this->getInstance();
$signerInstance->setCommonName('Test Signer');
$signerInstance->setPassword('123456');
foreach ($configuredFields as $field => $value) {
match ($field) {
'C' => $signerInstance->setCountry($value),
'ST' => $signerInstance->setState($value),
'L' => $signerInstance->setLocality($value),
'O' => $signerInstance->setOrganization($value),
};
}

$certificate = $signerInstance->generateCertificate();
$parsed = $signerInstance->readCertificate($certificate, '123456');
$subject = array_intersect_key($parsed['subject'], array_flip(['CN', 'C', 'ST', 'L', 'O']));
ksort($subject);
ksort($expectedSubject);
$this->assertSame($expectedSubject, $subject);
}

public static function leafSubjectCases(): array {
return [
'common name only' => [
[],
['CN' => 'Test Signer'],
],
'country and locality without defaults' => [
['C' => 'DE', 'L' => 'Berlin'],
['CN' => 'Test Signer', 'C' => 'DE', 'L' => 'Berlin'],
],
'state only' => [
['ST' => 'Brandenburg'],
['CN' => 'Test Signer', 'ST' => 'Brandenburg'],
],
'organization only' => [
['O' => 'LibreCode'],
['CN' => 'Test Signer', 'O' => 'LibreCode'],
],
'country locality and state' => [
['C' => 'DE', 'ST' => 'Brandenburg', 'L' => 'Berlin'],
['CN' => 'Test Signer', 'C' => 'DE', 'ST' => 'Brandenburg', 'L' => 'Berlin'],
],
'country locality and organization' => [
['C' => 'DE', 'L' => 'Berlin', 'O' => 'LibreCode'],
['CN' => 'Test Signer', 'C' => 'DE', 'L' => 'Berlin', 'O' => 'LibreCode'],
],
'all configured fields' => [
['C' => 'BR', 'ST' => 'Rio de Janeiro', 'L' => 'Niteroi', 'O' => 'LibreCode'],
['CN' => 'Test Signer', 'C' => 'BR', 'ST' => 'Rio de Janeiro', 'L' => 'Niteroi', 'O' => 'LibreCode'],
],
'placeholder strings explicitly supplied' => [
['ST' => 'Some-State', 'O' => 'Internet Widgits Pty Ltd'],
['CN' => 'Test Signer', 'ST' => 'Some-State', 'O' => 'Internet Widgits Pty Ltd'],
],
];
}

public function testLeafCsrUsesGeneratedConfig(): void {
$rootInstance = $this->getInstance();
$rootInstance->generateRootCert('Test Root CA', []);

$signerInstance = $this->getMockBuilder(OpenSslHandler::class)
->setConstructorArgs([
$this->config,
$this->appConfig,
$this->appDataFactory,
$this->dateTimeFormatter,
$this->tempManager,
$this->certificatePolicyService,
$this->urlGenerator,
$this->serialNumberService,
$this->caIdentifierService,
$this->policyService,
$this->logger,
$this->crlMapper,
$this->subjectAlternativeNameService,
$this->crlRevocationChecker,
])
->onlyMethods(['createCsr'])
->getMock();
$signerInstance->expects($this->once())->method('createCsr')
->willReturnCallback(function (array $distinguishedNames, mixed $privateKey, array $options) {
$this->assertArrayHasKey('config', $options);
$this->assertSame('req', $options['config_section_name'] ?? null);
$this->assertFileExists($options['config']);
$config = file_get_contents($options['config']);
$this->assertNotFalse($config);
$this->assertStringContainsString('[req_distinguished_name]', $config);
$this->assertStringContainsString('[v3_req]', $config);
$this->assertStringNotContainsString('req_extensions =', $config);
return openssl_csr_new($distinguishedNames, $privateKey, $options);
});

$signerInstance->setCommonName('Test Signer');
$signerInstance->setPassword('123456');
$certificate = $signerInstance->generateCertificate();
$parsed = $signerInstance->readCertificate($certificate, '123456');
$this->assertSame('Test Signer', $parsed['subject']['CN']);
}

public function testLeafCertificateUsesRsa2048Key(): void {
['certificate' => $certificate] = $this->generateLeafCertificate();

Expand Down
Loading