Initial commit
This commit is contained in:
commit
384646df40
|
@ -0,0 +1,12 @@
|
|||
vendor/
|
||||
composer.lock
|
||||
*~
|
||||
.php_cs.cache
|
||||
data/
|
||||
*.crt
|
||||
*.csr
|
||||
*.key
|
||||
*.pub
|
||||
.idea
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
$header = <<<EOF
|
||||
Self-Sign Cert
|
||||
Copyright (C) 2017 http://blog.rewiv.com eric@rewiv.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
EOF;
|
||||
|
||||
|
||||
return PhpCsFixer\Config::create()
|
||||
->setRiskyAllowed(true)
|
||||
->setRules(
|
||||
[
|
||||
'@Symfony' => true,
|
||||
'header_comment' => ['header' => $header],
|
||||
'ordered_class_elements' => true,
|
||||
'ordered_imports' => true,
|
||||
'no_mixed_echo_print' => ['use' => 'print'],
|
||||
'strict_param' => true,
|
||||
'strict_comparison' => true,
|
||||
'single_import_per_statement' => false,
|
||||
'phpdoc_order' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'phpdoc_add_missing_param_annotation' => true,
|
||||
'psr4' => true,
|
||||
'phpdoc_var_without_name' => false,
|
||||
'no_extra_consecutive_blank_lines' => [
|
||||
'break',
|
||||
'continue',
|
||||
'extra',
|
||||
'return',
|
||||
'throw',
|
||||
'parenthesis_brace_block',
|
||||
'square_brace_block',
|
||||
'curly_brace_block'
|
||||
],
|
||||
]
|
||||
)->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true)
|
||||
->name('*.php')
|
||||
->in([
|
||||
__DIR__,
|
||||
'src',
|
||||
'bin',
|
||||
])
|
||||
);
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Self-Sign Cert
|
||||
* Copyright (C) 2017 http://blog.rewiv.com eric@rewiv.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Sikofitt\Console\Command\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SignCertCommandTest extends TestCase
|
||||
{
|
||||
// TODO: Add tests
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "sikofitt/self-sign-cert",
|
||||
"description": "Easy self signed certs",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php":">=7.1",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"egulias/email-validator": "^2.1",
|
||||
"mledoze/countries": "^1.8",
|
||||
"phpseclib/phpseclib": "^2.0",
|
||||
"respect/validation": "^1.1",
|
||||
"symfony/console": "^3.2",
|
||||
"webmozart/json": "^1.2"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "R. Eric Wheeler",
|
||||
"email": "sikofitt@gmail.com"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sikofitt\\":"src/Sikofitt/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Sikofitt\\Tests\\":"Tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.1",
|
||||
"phpunit/phpunit": "^6.0",
|
||||
"symfony/var-dumper": "^3.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Self-Sign Cert
|
||||
* Copyright (C) 2017 http://blog.rewiv.com eric@rewiv.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
$application = new \Symfony\Component\Console\Application();
|
||||
|
||||
$dataDir = dirname(__DIR__.'/vendor/mledoze/countries/dist/countries.json');
|
||||
|
||||
$application->add(new Sikofitt\Console\Command\SignCertCommand(__DIR__));
|
||||
$application->add(new MLD\Console\Command\ExportCommand($dataDir.'/countries.json', __DIR__.'/data'));
|
||||
$application->run();
|
|
@ -0,0 +1,385 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Self-Sign Cert
|
||||
* Copyright (C) 2017 http://blog.rewiv.com eric@rewiv.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Sikofitt\Console\Command;
|
||||
|
||||
use Egulias\EmailValidator\EmailValidator;
|
||||
use Egulias\EmailValidator\Validation\DNSCheckValidation;
|
||||
use Egulias\EmailValidator\Validation\MultipleValidationWithAnd;
|
||||
use Egulias\EmailValidator\Validation\RFCValidation;
|
||||
use Egulias\EmailValidator\Validation\SpoofCheckValidation;
|
||||
use phpseclib\Crypt\RSA;
|
||||
use phpseclib\File\X509;
|
||||
use Sikofitt\PHPSecLib\Exception\PhpSecLibErrorToException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Webmozart\Json\JsonDecoder;
|
||||
|
||||
/**
|
||||
* Class SignCertCommand.
|
||||
*/
|
||||
class SignCertCommand extends Command
|
||||
{
|
||||
private $distinguishedName;
|
||||
private $rootDir;
|
||||
private $digestAlgo;
|
||||
private $privateKeyBits;
|
||||
private $domains = [];
|
||||
|
||||
/**
|
||||
* SignCertCommand constructor.
|
||||
*
|
||||
* @param string $rootDir
|
||||
*
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function __construct(string $rootDir)
|
||||
{
|
||||
parent::__construct(null);
|
||||
$this->rootDir = $rootDir;
|
||||
set_error_handler([$this, 'errorHandler']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PhpSecLibErrorToException
|
||||
*/
|
||||
public static function errorHandler()
|
||||
{
|
||||
[$errorNumber, $errorString, $errorFile, $errorLine] = func_get_args();
|
||||
|
||||
$message = sprintf(
|
||||
"%s\nError # %s\nFile : %s\nLine : %s",
|
||||
$errorString,
|
||||
$errorNumber,
|
||||
$errorFile,
|
||||
$errorLine
|
||||
);
|
||||
throw new PhpSecLibErrorToException($message, $errorNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function configure(): void
|
||||
{
|
||||
$hostname = gethostname();
|
||||
$this->setName('sign')
|
||||
->setAliases(['cert', 'sign:cert'])
|
||||
->setDescription('Self sign certificates easily.')
|
||||
->addOption(
|
||||
'years',
|
||||
'y',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Number of years for the certificate',
|
||||
10
|
||||
)
|
||||
->addOption(
|
||||
'csrname',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Signing request filename.',
|
||||
$hostname.'.csr'
|
||||
)
|
||||
->addOption(
|
||||
'keyname',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Private key filename.',
|
||||
$hostname.'.key'
|
||||
)
|
||||
->addOption(
|
||||
'pkeyname',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Public key filename.',
|
||||
$hostname.'.pub'
|
||||
)
|
||||
->addOption(
|
||||
'certname',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Certificate filename.',
|
||||
$hostname.'.crt'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @throws InvalidOptionException
|
||||
* @throws InvalidArgumentException
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function interact(
|
||||
InputInterface $input,
|
||||
OutputInterface $output
|
||||
): void {
|
||||
$jsonDecoder = new JsonDecoder();
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
try {
|
||||
$countries = (array) $jsonDecoder->decodeFile($this->rootDir.'/data/countries.json');
|
||||
} catch (\Exception $e) {
|
||||
throw new LogicException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
$choices = [];
|
||||
|
||||
foreach ($countries as $country) {
|
||||
/** @var array $altSpellings */
|
||||
$altSpellings = (array) $country->altSpellings;
|
||||
foreach ($altSpellings as $altSpelling) {
|
||||
if (preg_match('/\s/', $altSpelling)) {
|
||||
continue;
|
||||
}
|
||||
$twoLetterCode = $altSpelling; //substr($altSpelling, 0, 3);
|
||||
$choices[mb_strtolower($twoLetterCode)] = mb_strtoupper($twoLetterCode);
|
||||
}
|
||||
}
|
||||
|
||||
$countryQuestion = new Question('Country Name');
|
||||
$countryQuestion->setAutocompleterValues($choices);
|
||||
$countryQuestion->setNormalizer(function ($value) {
|
||||
return mb_strtoupper(trim($value));
|
||||
});
|
||||
|
||||
$countryQuestion->setValidator(function ($value) use ($choices) {
|
||||
if (false === in_array(mb_strtoupper($value), $choices, true)) {
|
||||
throw new InvalidOptionException($value.' is not a valid country choice.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
$countryName = $io->askQuestion($countryQuestion);
|
||||
$stateOrProvinceName = $io->ask('State or Province Name');
|
||||
$localityName = $io->ask('Locality Name');
|
||||
$organizationName = $io->ask('Organization Name');
|
||||
$organizationalUnitName = $io->ask('Organizational Unit Name');
|
||||
$commonName = $io->ask('Common Name', gethostname(), function ($value) {
|
||||
if (null === $value) {
|
||||
return gethostname();
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
$emailAddress = $io->ask('Email address (Optional)', null,
|
||||
function ($value) use ($io) {
|
||||
if (null === $value) {
|
||||
return $value;
|
||||
}
|
||||
$emailValidator = new EmailValidator();
|
||||
$validators = new MultipleValidationWithAnd([
|
||||
new RFCValidation(),
|
||||
new SpoofCheckValidation(),
|
||||
new DNSCheckValidation(),
|
||||
]);
|
||||
|
||||
if (true === $emailValidator->isValid($value, $validators)) {
|
||||
if ($emailValidator->hasWarnings()) {
|
||||
$io->warning($emailValidator->getWarnings());
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
throw new InvalidOptionException($emailValidator->getError()->getMessage());
|
||||
});
|
||||
|
||||
$signingAlgos = [
|
||||
'md2' => 'md2',
|
||||
'md5' => 'md5',
|
||||
'sha1' => 'sha1',
|
||||
'sha256' => 'sha256',
|
||||
'sha384' => 'sha384',
|
||||
'sha512' => 'sha512',
|
||||
];
|
||||
$this->domains = [$commonName];
|
||||
$domainQuestion = new Question('Domains (hit enter on a blank line to stop adding',
|
||||
false);
|
||||
$domainQuestion->setValidator(function ($value) {
|
||||
if (false !== $value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
while (false !== $domain = $io->askQuestion($domainQuestion)) {
|
||||
$this->domains[] = $domain;
|
||||
}
|
||||
|
||||
$digestAlgoQuestion = new ChoiceQuestion('Digest Method', $signingAlgos,
|
||||
'sha512');
|
||||
|
||||
$digestAlgoQuestion->setValidator(function ($value) use ($signingAlgos) {
|
||||
if (false === in_array($value, $signingAlgos,
|
||||
true)
|
||||
) {
|
||||
throw new InvalidOptionException('Invalid digest algorithm : '.$value.'.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
$digestAlgoQuestion->setAutocompleterValues($signingAlgos);
|
||||
$privateKeyBitsQuestion = new Question('Private key bits', 2048);
|
||||
$privateKeyBitsQuestion->setNormalizer(function ($value) {
|
||||
return (int) $value;
|
||||
});
|
||||
$privateKeyBitsQuestion->setValidator(function ($value) {
|
||||
if ($value < 384) {
|
||||
throw new InvalidOptionException('Private key bytes should be greater than 384.');
|
||||
}
|
||||
if (false === $this->validateBits($value, $this->digestAlgo)) {
|
||||
throw new InvalidOptionException('Not enough bits for algorithm '.$this->digestAlgo);
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
$this->digestAlgo = $io->askQuestion($digestAlgoQuestion);
|
||||
$this->privateKeyBits = $io->askQuestion($privateKeyBitsQuestion);
|
||||
|
||||
$this->distinguishedName = [
|
||||
'countryName' => $countryName,
|
||||
'stateOrProvinceName' => $stateOrProvinceName,
|
||||
'localityName' => $localityName,
|
||||
'organizationName' => $organizationName,
|
||||
'organizationalUnitName' => $organizationalUnitName,
|
||||
'commonName' => $commonName,
|
||||
];
|
||||
if (null !== $emailAddress) {
|
||||
$this->distinguishedName['emailAddress'] = $emailAddress;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function execute(
|
||||
InputInterface $input,
|
||||
OutputInterface $output
|
||||
): void {
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
if ($this->privateKeyBits > 4096) {
|
||||
$io->warning('Please wait, this could take awhile with '.$this->privateKeyBits.' bits.');
|
||||
}
|
||||
|
||||
$years = $input->getOption('years');
|
||||
$privateKeyObject = new RSA();
|
||||
$privateKeyObject->setEncryptionMode(RSA::ENCRYPTION_OAEP);
|
||||
$privateKeyObject->setHash($this->digestAlgo);
|
||||
$privateKeyObject->setMGFHash($this->digestAlgo);
|
||||
$privateKeyObject->setSignatureMode(RSA::SIGNATURE_PKCS1);
|
||||
$privateKeyParts = $privateKeyObject->createKey($this->privateKeyBits);
|
||||
$privateKey = $privateKeyParts['privatekey'];
|
||||
$publicKey = $privateKeyParts['publickey'];
|
||||
$privateKeyObject->loadKey($privateKey);
|
||||
|
||||
$pubKey = new RSA();
|
||||
$pubKey->loadKey($publicKey);
|
||||
$pubKey->setPublicKey();
|
||||
$subject = new X509();
|
||||
$subject->setStartDate('now');
|
||||
$subject->setEndDate('now +'.$years.' years');
|
||||
$subject->setDN($this->distinguishedName, true);
|
||||
if (count($this->domains) > 1) {
|
||||
call_user_func_array([$subject, 'setDomain'], $this->domains);
|
||||
}
|
||||
$subject->setPublicKey($pubKey);
|
||||
|
||||
$issuer = new X509();
|
||||
$issuer->setStartDate('now');
|
||||
$issuer->setEndDate('now +'.$years.' years');
|
||||
$issuer->setPrivateKey($privateKeyObject);
|
||||
$issuer->setDN($subject->getDN());
|
||||
|
||||
$x509 = new X509();
|
||||
$x509->setStartDate('now');
|
||||
$x509->setEndDate('now +'.$years.' years');
|
||||
$csr = $issuer->signCSR($this->prefixHash($this->digestAlgo));
|
||||
|
||||
$result = $x509->sign($issuer, $subject,
|
||||
$this->prefixHash($this->digestAlgo));
|
||||
|
||||
file_put_contents($input->getOption('keyname'), $privateKeyObject->getPrivateKey());
|
||||
file_put_contents($input->getOption('pkeyname'), $privateKeyObject->getPublicKey());
|
||||
file_put_contents($input->getOption('csrname'), $x509->saveCSR($csr));
|
||||
file_put_contents($input->getOption('certname'), $x509->saveX509($result));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $bits
|
||||
* @param string $algo
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function validateBits(int $bits, string $algo): bool
|
||||
{
|
||||
// see http://tools.ietf.org/html/rfc3447#page-43
|
||||
switch ($algo) {
|
||||
case 'sha256':
|
||||
if ($bits < 496) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
break;
|
||||
case 'sha384':
|
||||
if ($bits < 624) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
break;
|
||||
case 'sha512':
|
||||
if ($bits < 752) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hash
|
||||
* The hash to prefix
|
||||
*
|
||||
* @return string
|
||||
* The hash prefixed with 'WithRSAEncryption'
|
||||
*/
|
||||
private function prefixHash(string $hash): string
|
||||
{
|
||||
return sprintf('%sWithRSAEncryption', $hash);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Self-Sign Cert
|
||||
* Copyright (C) 2017 http://blog.rewiv.com eric@rewiv.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Sikofitt\PHPSecLib\Exception;
|
||||
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
|
||||
/**
|
||||
* Class PhpSecLibErrorToException.
|
||||
*/
|
||||
class PhpSecLibErrorToException extends \Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
Loading…
Reference in New Issue