Skip to content

Commit 1d15c47

Browse files
committed
Add digest and cipher options
1 parent e1aa9bd commit 1d15c47

File tree

8 files changed

+434
-1
lines changed

8 files changed

+434
-1
lines changed

src/Console/Command/GeneratePrivateKeyCommand.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
namespace Skriptfabrik\Openssl\Console\Command;
99

1010
use Skriptfabrik\Openssl\Console\Input\BitsOptionTrait;
11+
use Skriptfabrik\Openssl\Console\Input\CipherOptionTrait;
12+
use Skriptfabrik\Openssl\Console\Input\DigestOptionTrait;
1113
use Skriptfabrik\Openssl\Console\Input\NoOverrideOptionTrait;
1214
use Skriptfabrik\Openssl\Console\Input\OutputArgumentTrait;
1315
use Skriptfabrik\Openssl\Console\Input\PassphraseOptionTrait;
@@ -29,9 +31,11 @@
2931
*/
3032
class GeneratePrivateKeyCommand extends Command
3133
{
34+
use DigestOptionTrait;
3235
use TypeOptionTrait;
3336
use BitsOptionTrait;
3437
use PassphraseOptionTrait;
38+
use CipherOptionTrait;
3539
use NoOverrideOptionTrait;
3640
use OutputArgumentTrait;
3741

@@ -45,6 +49,11 @@ class GeneratePrivateKeyCommand extends Command
4549
*/
4650
public const DESCRIPTION = 'Generate a private key with OpenSSL';
4751

52+
/**
53+
* The default digest.
54+
*/
55+
public const DEFAULT_DIGEST = 'sha256';
56+
4857
/**
4958
* The default key type.
5059
*/
@@ -65,6 +74,13 @@ protected function configure(): void
6574
{
6675
$this->setName(self::NAME);
6776
$this->setDescription(self::DESCRIPTION);
77+
$this->addOption(
78+
'digest',
79+
'd',
80+
InputOption::VALUE_REQUIRED,
81+
'The method or signature hash',
82+
self::DEFAULT_DIGEST
83+
);
6884
$this->addOption(
6985
'type',
7086
't',
@@ -85,6 +101,12 @@ protected function configure(): void
85101
InputOption::VALUE_REQUIRED,
86102
'The private key can be optionally protected by a passphrase'
87103
);
104+
$this->addOption(
105+
'cipher',
106+
'c',
107+
InputOption::VALUE_REQUIRED,
108+
'The cipher for the passphrase protection'
109+
);
88110
$this->addOption(
89111
'no-override',
90112
null,
@@ -111,9 +133,11 @@ protected function configure(): void
111133
*/
112134
protected function execute(InputInterface $input, OutputInterface $output): int
113135
{
136+
$digest = $this->getDigestOption($input);
114137
$type = $this->getTypeOption($input);
115138
$bits = $this->getBitsOption($input);
116139
$passphrase = $this->getPassphraseOption($input);
140+
$cipher = $this->getCipherOption($input);
117141
$noOverride = $this->isNoOverrideOption($input);
118142
$outputFile = $this->getOutputArgument($input);
119143

@@ -135,7 +159,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
135159
$generator = $this->createPrivateKeyGenerator();
136160

137161
try {
138-
$generator->setType($type)->setBits($bits)->setPassphrase($passphrase);
162+
$generator->setDigest($digest)->setType($type)->setBits($bits)->setPassphrase($passphrase)->setCipher($cipher);
139163
} catch (InvalidArgumentException $exception) {
140164
$output->writeln(sprintf('<error>[OpenSSL] %s</error>', $exception->getMessage()));
141165
return 1;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* This file is part of the skriptfabrik PHP OpenSSL package.
4+
*
5+
* @author Daniel Schröder <[email protected]>
6+
*/
7+
8+
namespace Skriptfabrik\Openssl\Console\Input;
9+
10+
use Symfony\Component\Console\Exception\InvalidOptionException;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use function is_string;
13+
14+
/**
15+
* Cipher option trait.
16+
*
17+
* @package Skriptfabrik\Openssl\Console\Input
18+
*/
19+
trait CipherOptionTrait
20+
{
21+
/**
22+
* Get cipher option.
23+
*
24+
* @param InputInterface $input
25+
*
26+
* @return string|null
27+
*
28+
* @throws \Symfony\Component\Console\Exception\InvalidOptionException
29+
*/
30+
protected function getCipherOption(InputInterface $input): ?string
31+
{
32+
$cipher = $input->getOption('cipher');
33+
if ($cipher === null || is_string($cipher)) {
34+
return $cipher;
35+
}
36+
37+
throw new InvalidOptionException('The "--cipher" option must be string');
38+
}
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* This file is part of the skriptfabrik PHP OpenSSL package.
4+
*
5+
* @author Daniel Schröder <[email protected]>
6+
*/
7+
8+
namespace Skriptfabrik\Openssl\Console\Input;
9+
10+
use Symfony\Component\Console\Exception\InvalidOptionException;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use function is_string;
13+
14+
/**
15+
* Digest option trait.
16+
*
17+
* @package Skriptfabrik\Openssl\Console\Input
18+
*/
19+
trait DigestOptionTrait
20+
{
21+
/**
22+
* Get digest option.
23+
*
24+
* @param InputInterface $input
25+
*
26+
* @return string
27+
*
28+
* @throws \Symfony\Component\Console\Exception\InvalidOptionException
29+
*/
30+
protected function getDigestOption(InputInterface $input): string
31+
{
32+
$digest = $input->getOption('digest');
33+
if (is_string($digest)) {
34+
return $digest;
35+
}
36+
37+
throw new InvalidOptionException('The "--digest" option must be string');
38+
}
39+
}

src/Generator/PrivateKeyGenerator.php

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,46 @@ class PrivateKeyGenerator
2323
*/
2424
public const MIN_BITS = 64;
2525

26+
/**
27+
* RC2 128 cipher.
28+
*/
29+
public const CIPHER_RC2_128 = 'rc2-128';
30+
31+
/**
32+
* AES 128 CBC cipher.
33+
*/
34+
public const CIPHER_AES_128_CBC = 'aes-128-cbc';
35+
36+
/**
37+
* AES 192 CBC cipher.
38+
*/
39+
public const CIPHER_AES_192_CBC = 'aes-192-cbc';
40+
41+
/**
42+
* AES 256 CBC cipher.
43+
*/
44+
public const CIPHER_AES_256_CBC = 'aes-256-cbc';
45+
46+
/**
47+
* Supported ciphers.
48+
*/
49+
public const SUPPORTED_CIPHERS = [
50+
OPENSSL_CIPHER_RC2_128 => self::CIPHER_RC2_128,
51+
OPENSSL_CIPHER_AES_128_CBC => self::CIPHER_AES_128_CBC,
52+
OPENSSL_CIPHER_AES_192_CBC => self::CIPHER_AES_192_CBC,
53+
OPENSSL_CIPHER_AES_256_CBC => self::CIPHER_AES_256_CBC,
54+
];
55+
56+
/**
57+
* @var string[]|null
58+
*/
59+
private static $supportedDigests;
60+
61+
/**
62+
* @var string
63+
*/
64+
private $digest = 'sha256';
65+
2666
/**
2767
* @var int
2868
*/
@@ -38,6 +78,59 @@ class PrivateKeyGenerator
3878
*/
3979
private $passphrase;
4080

81+
/**
82+
* @var int|null
83+
*/
84+
private $cipher;
85+
86+
/**
87+
* @return string[]
88+
*/
89+
public function getSupportedDigests(): array
90+
{
91+
if (self::$supportedDigests === null) {
92+
self::$supportedDigests = openssl_get_md_methods(true);
93+
}
94+
95+
return self::$supportedDigests;
96+
}
97+
98+
/**
99+
* Get digest.
100+
*
101+
* @return string
102+
*/
103+
public function getDigest(): string
104+
{
105+
return $this->digest;
106+
}
107+
108+
/**
109+
* Set digest.
110+
*
111+
* @param string $digest
112+
*
113+
* @return $this
114+
*
115+
* @throws InvalidArgumentException
116+
*/
117+
public function setDigest(string $digest): self
118+
{
119+
if (!in_array($digest, $this->getSupportedDigests(), true)) {
120+
throw new InvalidArgumentException(
121+
sprintf(
122+
'Invalid digest (%s), valid digests: %s',
123+
$digest,
124+
implode(', ', $this->getSupportedDigests())
125+
)
126+
);
127+
}
128+
129+
$this->digest = $digest;
130+
131+
return $this;
132+
}
133+
41134
/**
42135
* Get type.
43136
*
@@ -142,6 +235,50 @@ public function setPassphrase(?string $passphrase): self
142235
return $this;
143236
}
144237

238+
/**
239+
* Get cipher.
240+
*
241+
* @return string|null
242+
*/
243+
public function getCipher(): ?string
244+
{
245+
return self::SUPPORTED_CIPHERS[$this->cipher] ?? null;
246+
}
247+
248+
/**
249+
* Set cipher.
250+
*
251+
* @param string|null $cipher
252+
*
253+
* @return $this
254+
*
255+
* @throws InvalidArgumentException
256+
*/
257+
public function setCipher(?string $cipher): self
258+
{
259+
if ($cipher === null) {
260+
$this->cipher = null;
261+
262+
return $this;
263+
}
264+
265+
$numericCipher = array_search(strtolower($cipher), self::SUPPORTED_CIPHERS, true);
266+
267+
if ($numericCipher === false) {
268+
throw new InvalidArgumentException(
269+
sprintf(
270+
'Invalid cipher (%s), valid ciphers: %s',
271+
$cipher,
272+
implode(', ', self::SUPPORTED_CIPHERS)
273+
)
274+
);
275+
}
276+
277+
$this->cipher = $numericCipher;
278+
279+
return $this;
280+
}
281+
145282
/**
146283
* Generate private key.
147284
*
@@ -153,9 +290,11 @@ public function generate(): PrivateKey
153290
{
154291
$key = openssl_pkey_new(
155292
[
293+
'digest_alg' => $this->digest,
156294
'private_key_type' => $this->type,
157295
'private_key_bits' => $this->bits,
158296
'encrypt_key' => $this->passphrase !== null,
297+
'encrypt_key_cipher' => $this->cipher,
159298
]
160299
);
161300

tests/Console/Command/GeneratePrivateKeyCommandTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ public function testConfiguration(): void
3131

3232
$this->assertSame(GeneratePrivateKeyCommand::NAME, $command->getName());
3333
$this->assertSame(GeneratePrivateKeyCommand::DESCRIPTION, $command->getDescription());
34+
$this->assertTrue($command->getDefinition()->hasOption('digest'));
3435
$this->assertTrue($command->getDefinition()->hasOption('type'));
3536
$this->assertTrue($command->getDefinition()->hasOption('bits'));
37+
$this->assertTrue($command->getDefinition()->hasOption('passphrase'));
38+
$this->assertTrue($command->getDefinition()->hasOption('cipher'));
39+
$this->assertTrue($command->getDefinition()->hasOption('no-override'));
3640
$this->assertTrue($command->getDefinition()->hasArgument('output'));
3741
}
3842

@@ -50,6 +54,7 @@ public function testSuccessfulExecution(): void
5054
[
5155
'command' => $command->getName(),
5256
'output' => $output->getPathname(),
57+
'--digest' => 'sha256',
5358
'--type' => 'DSA',
5459
'--bits' => '1024',
5560
]
@@ -120,6 +125,31 @@ public function testExecutionReturnsErrorOnWriteFailure(): void
120125
$this->assertContains('[OpenSSL] Unable to write private key file', $commandTester->getDisplay());
121126
}
122127

128+
public function testExecutionReturnsErrorOnInvalidDigestOption(): void
129+
{
130+
$application = new Application();
131+
$application->add(new GeneratePrivateKeyCommand());
132+
133+
$command = $application->find(GeneratePrivateKeyCommand::NAME);
134+
$commandTester = new CommandTester($command);
135+
136+
$output = TempFileObjectHelper::createTempFile();
137+
$output->fwrite('');
138+
139+
$commandTester->execute(
140+
[
141+
'command' => $command->getName(),
142+
'output' => $output->getPathname(),
143+
'--digest' => 'FOO',
144+
]
145+
);
146+
147+
$this->assertEquals(1, $commandTester->getStatusCode());
148+
$this->assertContains('[OpenSSL] Invalid digest (FOO)', $commandTester->getDisplay());
149+
150+
unlink($output->getPathname());
151+
}
152+
123153
public function testExecutionReturnsErrorOnInvalidTypeOption(): void
124154
{
125155
$application = new Application();
@@ -181,9 +211,11 @@ public function testExecutionReturnsErrorOnGeneratorFailure(): void
181211
};
182212

183213
$privateKeyGeneratorProphecy = $this->prophesize(PrivateKeyGenerator::class);
214+
$privateKeyGeneratorProphecy->setDigest('sha256')->will($returnSelf);
184215
$privateKeyGeneratorProphecy->setType(KeyInterface::TYPE_RSA)->will($returnSelf);
185216
$privateKeyGeneratorProphecy->setBits(2048)->will($returnSelf);
186217
$privateKeyGeneratorProphecy->setPassphrase(null)->will($returnSelf);
218+
$privateKeyGeneratorProphecy->setCipher(null)->will($returnSelf);
187219
$privateKeyGeneratorProphecy->generate()->willThrow(new OpensslErrorException('Unknown OpenSSL error'));
188220

189221
$commandMock = $this->getMockBuilder(GeneratePrivateKeyCommand::class)

0 commit comments

Comments
 (0)