This change requires the usage of a path instead of the App ID when signing code. This has the advantage that developers can also sign code under a different location to make it easier. (e.g. remove `.git`, …) Also it adds an example command usage as well as a link to the documentationtags/v9.0beta1
@@ -23,6 +23,7 @@ namespace OC\Core\Command\Integrity; | |||
use OC\IntegrityCheck\Checker; | |||
use OC\IntegrityCheck\Helpers\FileAccessHelper; | |||
use OCP\IURLGenerator; | |||
use phpseclib\Crypt\RSA; | |||
use phpseclib\File\X509; | |||
use Symfony\Component\Console\Command\Command; | |||
@@ -40,23 +41,28 @@ class SignApp extends Command { | |||
private $checker; | |||
/** @var FileAccessHelper */ | |||
private $fileAccessHelper; | |||
/** @var IURLGenerator */ | |||
private $urlGenerator; | |||
/** | |||
* @param Checker $checker | |||
* @param FileAccessHelper $fileAccessHelper | |||
* @param IURLGenerator $urlGenerator | |||
*/ | |||
public function __construct(Checker $checker, | |||
FileAccessHelper $fileAccessHelper) { | |||
FileAccessHelper $fileAccessHelper, | |||
IURLGenerator $urlGenerator) { | |||
parent::__construct(null); | |||
$this->checker = $checker; | |||
$this->fileAccessHelper = $fileAccessHelper; | |||
$this->urlGenerator = $urlGenerator; | |||
} | |||
protected function configure() { | |||
$this | |||
->setName('integrity:sign-app') | |||
->setDescription('Sign app using a private key.') | |||
->addOption('appId', null, InputOption::VALUE_REQUIRED, 'Application to sign') | |||
->setDescription('Signs an app using a private key.') | |||
->addOption('path', null, InputOption::VALUE_REQUIRED, 'Application to sign') | |||
->addOption('privateKey', null, InputOption::VALUE_REQUIRED, 'Path to private key to use for signing') | |||
->addOption('certificate', null, InputOption::VALUE_REQUIRED, 'Path to certificate to use for signing'); | |||
} | |||
@@ -65,11 +71,14 @@ class SignApp extends Command { | |||
* {@inheritdoc } | |||
*/ | |||
protected function execute(InputInterface $input, OutputInterface $output) { | |||
$appId = $input->getOption('appId'); | |||
$path = $input->getOption('path'); | |||
$privateKeyPath = $input->getOption('privateKey'); | |||
$keyBundlePath = $input->getOption('certificate'); | |||
if(is_null($appId) || is_null($privateKeyPath) || is_null($keyBundlePath)) { | |||
$output->writeln('--appId, --privateKey and --certificate are required.'); | |||
if(is_null($path) || is_null($privateKeyPath) || is_null($keyBundlePath)) { | |||
$documentationUrl = $this->urlGenerator->linkToDocs('developer-code-integrity'); | |||
$output->writeln('This command requires the --path, --privateKey and --certificate.'); | |||
$output->writeln('Example: ./occ integrity:sign-app --path="/Users/lukasreschke/Programming/myapp/" --privateKey="/Users/lukasreschke/private/myapp.key" --certificate="/Users/lukasreschke/public/mycert.crt"'); | |||
$output->writeln('For more information please consult the documentation: '. $documentationUrl); | |||
return null; | |||
} | |||
@@ -91,8 +100,8 @@ class SignApp extends Command { | |||
$x509 = new X509(); | |||
$x509->loadX509($keyBundle); | |||
$x509->setPrivateKey($rsa); | |||
$this->checker->writeAppSignature($appId, $x509, $rsa); | |||
$this->checker->writeAppSignature($path, $x509, $rsa); | |||
$output->writeln('Successfully signed "'.$appId.'"'); | |||
$output->writeln('Successfully signed "'.$path.'"'); | |||
} | |||
} |
@@ -36,7 +36,8 @@ $application->add(new OC\Core\Command\App\CheckCode($infoParser)); | |||
$application->add(new OC\Core\Command\L10n\CreateJs()); | |||
$application->add(new \OC\Core\Command\Integrity\SignApp( | |||
\OC::$server->getIntegrityCodeChecker(), | |||
new \OC\IntegrityCheck\Helpers\FileAccessHelper() | |||
new \OC\IntegrityCheck\Helpers\FileAccessHelper(), | |||
\OC::$server->getURLGenerator() | |||
)); | |||
$application->add(new \OC\Core\Command\Integrity\SignCore( | |||
\OC::$server->getIntegrityCodeChecker(), |
@@ -114,6 +114,7 @@ class Checker { | |||
* | |||
* @param string $folderToIterate | |||
* @return \RecursiveIteratorIterator | |||
* @throws \Exception | |||
*/ | |||
private function getFolderIterator($folderToIterate) { | |||
$dirItr = new \RecursiveDirectoryIterator( | |||
@@ -189,17 +190,19 @@ class Checker { | |||
} | |||
/** | |||
* Write the signature of the specified app | |||
* Write the signature of the app in the specified folder | |||
* | |||
* @param string $appId | |||
* @param string $path | |||
* @param X509 $certificate | |||
* @param RSA $privateKey | |||
* @throws \Exception | |||
*/ | |||
public function writeAppSignature($appId, | |||
public function writeAppSignature($path, | |||
X509 $certificate, | |||
RSA $privateKey) { | |||
$path = $this->appLocator->getAppPath($appId); | |||
if(!is_dir($path)) { | |||
throw new \Exception('Directory does not exist.'); | |||
} | |||
$iterator = $this->getFolderIterator($path); | |||
$hashes = $this->generateHashes($iterator, $path); | |||
$signature = $this->createSignatureData($hashes, $certificate, $privateKey); |
@@ -23,6 +23,7 @@ namespace Test\Command\Integrity; | |||
use OC\Core\Command\Integrity\SignApp; | |||
use OC\IntegrityCheck\Checker; | |||
use OC\IntegrityCheck\Helpers\FileAccessHelper; | |||
use OCP\IURLGenerator; | |||
use Test\TestCase; | |||
class SignAppTest extends TestCase { | |||
@@ -32,6 +33,8 @@ class SignAppTest extends TestCase { | |||
private $signApp; | |||
/** @var FileAccessHelper */ | |||
private $fileAccessHelper; | |||
/** @var IURLGenerator */ | |||
private $urlGenerator; | |||
public function setUp() { | |||
parent::setUp(); | |||
@@ -39,20 +42,23 @@ class SignAppTest extends TestCase { | |||
->disableOriginalConstructor()->getMock(); | |||
$this->fileAccessHelper = $this->getMockBuilder('\OC\IntegrityCheck\Helpers\FileAccessHelper') | |||
->disableOriginalConstructor()->getMock(); | |||
$this->urlGenerator = $this->getMockBuilder('\OCP\IURLGenerator') | |||
->disableOriginalConstructor()->getMock(); | |||
$this->signApp = new SignApp( | |||
$this->checker, | |||
$this->fileAccessHelper | |||
$this->fileAccessHelper, | |||
$this->urlGenerator | |||
); | |||
} | |||
public function testExecuteWithMissingAppId() { | |||
public function testExecuteWithMissingPath() { | |||
$inputInterface = $this->getMock('\Symfony\Component\Console\Input\InputInterface'); | |||
$outputInterface = $this->getMock('\Symfony\Component\Console\Output\OutputInterface'); | |||
$inputInterface | |||
->expects($this->at(0)) | |||
->method('getOption') | |||
->with('appId') | |||
->with('path') | |||
->will($this->returnValue(null)); | |||
$inputInterface | |||
->expects($this->at(1)) | |||
@@ -68,7 +74,7 @@ class SignAppTest extends TestCase { | |||
$outputInterface | |||
->expects($this->at(0)) | |||
->method('writeln') | |||
->with('--appId, --privateKey and --certificate are required.'); | |||
->with('This command requires the --path, --privateKey and --certificate.'); | |||
$this->invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface]); | |||
} | |||
@@ -80,7 +86,7 @@ class SignAppTest extends TestCase { | |||
$inputInterface | |||
->expects($this->at(0)) | |||
->method('getOption') | |||
->with('appId') | |||
->with('path') | |||
->will($this->returnValue('AppId')); | |||
$inputInterface | |||
->expects($this->at(1)) | |||
@@ -94,9 +100,9 @@ class SignAppTest extends TestCase { | |||
->will($this->returnValue('Certificate')); | |||
$outputInterface | |||
->expects($this->at(0)) | |||
->method('writeln') | |||
->with('--appId, --privateKey and --certificate are required.'); | |||
->expects($this->at(0)) | |||
->method('writeln') | |||
->with('This command requires the --path, --privateKey and --certificate.'); | |||
$this->invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface]); | |||
} | |||
@@ -108,7 +114,7 @@ class SignAppTest extends TestCase { | |||
$inputInterface | |||
->expects($this->at(0)) | |||
->method('getOption') | |||
->with('appId') | |||
->with('path') | |||
->will($this->returnValue('AppId')); | |||
$inputInterface | |||
->expects($this->at(1)) | |||
@@ -124,7 +130,7 @@ class SignAppTest extends TestCase { | |||
$outputInterface | |||
->expects($this->at(0)) | |||
->method('writeln') | |||
->with('--appId, --privateKey and --certificate are required.'); | |||
->with('This command requires the --path, --privateKey and --certificate.'); | |||
$this->invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface]); | |||
} | |||
@@ -136,7 +142,7 @@ class SignAppTest extends TestCase { | |||
$inputInterface | |||
->expects($this->at(0)) | |||
->method('getOption') | |||
->with('appId') | |||
->with('path') | |||
->will($this->returnValue('AppId')); | |||
$inputInterface | |||
->expects($this->at(1)) | |||
@@ -170,7 +176,7 @@ class SignAppTest extends TestCase { | |||
$inputInterface | |||
->expects($this->at(0)) | |||
->method('getOption') | |||
->with('appId') | |||
->with('path') | |||
->will($this->returnValue('AppId')); | |||
$inputInterface | |||
->expects($this->at(1)) | |||
@@ -209,7 +215,7 @@ class SignAppTest extends TestCase { | |||
$inputInterface | |||
->expects($this->at(0)) | |||
->method('getOption') | |||
->with('appId') | |||
->with('path') | |||
->will($this->returnValue('AppId')); | |||
$inputInterface | |||
->expects($this->at(1)) |
@@ -77,7 +77,7 @@ class CheckerTest extends TestCase { | |||
/** | |||
* @expectedException \Exception | |||
* @expectedExceptionMessage Directory name must not be empty. | |||
* @expectedExceptionMessage Directory does not exist. | |||
*/ | |||
public function testWriteAppSignatureOfNotExistingApp() { | |||
$keyBundle = file_get_contents(__DIR__ .'/../../data/integritycheck/SomeApp.crt'); | |||
@@ -98,11 +98,6 @@ class CheckerTest extends TestCase { | |||
"signature": "Y5yvXvcGHVPuRRatKVDUONWq1FpLXugZd6Km\/+aEHsQj7coVl9FeMj9OsWamBf7yRIw3dtNLguTLlAA9QAv\/b0uHN3JnbNZN+dwFOve4NMtqXfSDlWftqKN00VS+RJXpG1S2IIx9Poyp2NoghL\/5AuTv4GHiNb7zU\/DT\/kt71pUGPgPR6IIFaE+zHOD96vjYkrH+GfWZzKR0FCdLib9yyNvk+EGrcjKM6qjs2GKfS\/XFjj\/\/neDnh\/0kcPuKE3ZbofnI4TIDTv0CGqvOp7PtqVNc3Vy\/UKa7uF1PT0MAUKMww6EiMUSFZdUVP4WWF0Y72W53Qdtf1hrAZa2kfKyoK5kd7sQmCSKUPSU8978AUVZlBtTRlyT803IKwMV0iHMkw+xYB1sN2FlHup\/DESADqxhdgYuK35bCPvgkb4SBe4B8Voz\/izTvcP7VT5UvkYdAO+05\/jzdaHEmzmsD92CFfvX0q8O\/Y\/29ubftUJsqcHeMDKgcR4eZOE8+\/QVc\/89QO6WnKNuNuV+5bybO6g6PAdC9ZPsCvnihS61O2mwRXHLR3jv2UleFWm+lZEquPKtkhi6SLtDiijA4GV6dmS+dzujSLb7hGeD5o1plZcZ94uhWljl+QIp82+zU\/lYB1Zfr4Mb4e+V7r2gv7Fbv7y6YtjE2GIQwRhC5jq56bD0ZB+I=", | |||
"certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEwTCCAqmgAwIBAgIUWv0iujufs5lUr0svCf\/qTQvoyKAwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIyNDk1M1oXDTE2MTEwMzIyNDk1M1owEjEQMA4GA1UEAwwHU29tZUFwcDCCAiIw\r\nDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK8q0x62agGSRBqeWsaeEwFfepMk\r\nF8cAobMMi50qHCv9IrOn\/ZH9l52xBrbIkErVmRjmly0d4JhD8Ymhidsh9ONKYl\/j\r\n+ishsZDM8eNNdp3Ew+fEYVvY1W7mR1qU24NWj0bzVsClI7hvPVIuw7AjfBDq1C5+\r\nA+ZSLSXYvOK2cEWjdxQfuNZwEZSjmA63DUllBIrm35IaTvfuyhU6BW9yHZxmb8+M\r\nw0xDv30D5UkE\/2N7Pa\/HQJLxCR+3zKibRK3nUyRDLSXxMkU9PnFNaPNX59VPgyj4\r\nGB1CFSToldJVPF4pzh7p36uGXZVxs8m3LFD4Ol8mhi7jkxDZjqFN46gzR0r23Py6\r\ndol9vfawGIoUwp9LvL0S7MvdRY0oazLXwClLP4OQ17zpSMAiCj7fgNT661JamPGj\r\nt5O7Zn2wA7I4ddDS\/HDTWCu98Zwc9fHIpsJPgCZ9awoqxi4Mnf7Pk9g5nnXhszGC\r\ncxxIASQKM+GhdzoRxKknax2RzUCwCzcPRtCj8AQT\/x\/mqN3PfRmlnFBNACUw9bpZ\r\nSOoNq2pCF9igftDWpSIXQ38pVpKLWowjjg3DVRmVKBgivHnUnVLyzYBahHPj0vaz\r\ntFtUFRaqXDnt+4qyUGyrT5h5pjZaTcHIcSB4PiarYwdVvgslgwnQzOUcGAzRWBD4\r\n6jV2brP5vFY3g6iPAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBACTY3CCHC+Z28gCf\r\nFWGKQ3wAKs+k4+0yoti0qm2EKX7rSGQ0PHSas6uW79WstC4Rj+DYkDtIhGMSg8FS\r\nHVGZHGBCc0HwdX+BOAt3zi4p7Sf3oQef70\/4imPoKxbAVCpd\/cveVcFyDC19j1yB\r\nBapwu87oh+muoeaZxOlqQI4UxjBlR\/uRSMhOn2UGauIr3dWJgAF4pGt7TtIzt+1v\r\n0uA6FtN1Y4R5O8AaJPh1bIG0CVvFBE58esGzjEYLhOydgKFnEP94kVPgJD5ds9C3\r\npPhEpo1dRpiXaF7WGIV1X6DI\/ipWvfrF7CEy6I\/kP1InY\/vMDjQjeDnJ\/VrXIWXO\r\nyZvHXVaN\/m+1RlETsH7YO\/QmxRue9ZHN3gvvWtmpCeA95sfpepOk7UcHxHZYyQbF\r\n49\/au8j+5tsr4A83xzsT1JbcKRxkAaQ7WDJpOnE5O1+H0fB+BaLakTg6XX9d4Fo7\r\n7Gin7hVWX7pL+JIyxMzME3LhfI61+CRcqZQIrpyaafUziPQbWIPfEs7h8tCOWyvW\r\nUO8ZLervYCB3j44ivkrxPlcBklDCqqKKBzDP9dYOtS\/P4RB1NkHA9+NTvmBpTonS\r\nSFXdg9fFMD7VfjDE3Vnk+8DWkVH5wBYowTAD7w9Wuzr7DumiAULexnP\/Y7xwxLv7\r\n4B+pXTAcRK0zECDEaX3npS8xWzrB\r\n-----END CERTIFICATE-----" | |||
}'; | |||
$this->appLocator | |||
->expects($this->once()) | |||
->method('getAppPath') | |||
->with('SomeExistingApp') | |||
->will($this->returnValue(\OC::$SERVERROOT . '/tests/data/integritycheck/app/')); | |||
$this->fileAccessHelper | |||
->expects($this->once()) | |||
->method('file_put_contents') | |||
@@ -117,7 +112,7 @@ class CheckerTest extends TestCase { | |||
$rsa->loadKey($rsaPrivateKey); | |||
$x509 = new X509(); | |||
$x509->loadX509($keyBundle); | |||
$this->checker->writeAppSignature('SomeExistingApp', $x509, $rsa); | |||
$this->checker->writeAppSignature(\OC::$SERVERROOT . '/tests/data/integritycheck/app/', $x509, $rsa); | |||
} | |||
public function testVerifyAppSignatureWithoutSignatureData() { |